mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-22 22:41:27 +00:00
Merge pull request #250 from dansup/frontend-ui-refactor
Frontend ui refactor
This commit is contained in:
commit
02426d8c55
41 changed files with 3253 additions and 78 deletions
9
.editorconfig
Normal file
9
.editorconfig
Normal file
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Carbon\Carbon;
|
||||
use Auth, Cache, Redis;
|
||||
use App\{Notification, Profile, User};
|
||||
|
||||
|
@ -15,10 +16,17 @@ class AccountController extends Controller
|
|||
|
||||
public function notifications(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'page' => 'nullable|min:1|max:3'
|
||||
]);
|
||||
$profile = Auth::user()->profile;
|
||||
//$notifications = $this->fetchNotifications($profile->id);
|
||||
$timeago = Carbon::now()->subMonths(6);
|
||||
$notifications = Notification::whereProfileId($profile->id)
|
||||
->orderBy('id','desc')->take(30)->simplePaginate();
|
||||
->whereDate('created_at', '>', $timeago)
|
||||
->orderBy('id','desc')
|
||||
->take(30)
|
||||
->simplePaginate();
|
||||
|
||||
return view('account.activity', compact('profile', 'notifications'));
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ class ProfileController extends Controller
|
|||
// TODO: refactor this mess
|
||||
$owner = Auth::check() && Auth::id() === $user->user_id;
|
||||
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
||||
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
|
||||
$timeline = $user->statuses()
|
||||
->whereHas('media')
|
||||
->whereNull('in_reply_to_id')
|
||||
|
@ -39,7 +40,7 @@ class ProfileController extends Controller
|
|||
->withCount(['comments', 'likes'])
|
||||
->simplePaginate(21);
|
||||
|
||||
return view('profile.show', compact('user', 'owner', 'is_following', 'timeline'));
|
||||
return view('profile.show', compact('user', 'owner', 'is_following', 'is_admin', 'timeline'));
|
||||
}
|
||||
|
||||
public function showActivityPub(Request $request, $user)
|
||||
|
@ -66,7 +67,8 @@ class ProfileController extends Controller
|
|||
$owner = Auth::check() && Auth::id() === $user->user_id;
|
||||
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
||||
$followers = $profile->followers()->orderBy('created_at','desc')->simplePaginate(12);
|
||||
return view('profile.followers', compact('user', 'profile', 'followers', 'owner', 'is_following'));
|
||||
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
|
||||
return view('profile.followers', compact('user', 'profile', 'followers', 'owner', 'is_following', 'is_admin'));
|
||||
}
|
||||
|
||||
public function following(Request $request, $username)
|
||||
|
@ -77,7 +79,8 @@ class ProfileController extends Controller
|
|||
$owner = Auth::check() && Auth::id() === $user->user_id;
|
||||
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
||||
$following = $profile->following()->orderBy('created_at','desc')->simplePaginate(12);
|
||||
return view('profile.following', compact('user', 'profile', 'following', 'owner', 'is_following'));
|
||||
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
|
||||
return view('profile.following', compact('user', 'profile', 'following', 'owner', 'is_following', 'is_admin'));
|
||||
}
|
||||
|
||||
public function savedBookmarks(Request $request, $username)
|
||||
|
@ -88,7 +91,9 @@ class ProfileController extends Controller
|
|||
$user = Auth::user()->profile;
|
||||
$owner = true;
|
||||
$following = false;
|
||||
$timeline = $user->bookmarks()->orderBy('created_at','desc')->simplePaginate(10);
|
||||
return view('profile.show', compact('user', 'owner', 'following', 'timeline'));
|
||||
$timeline = $user->bookmarks()->withCount(['likes','comments'])->orderBy('created_at','desc')->simplePaginate(10);
|
||||
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
||||
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
|
||||
return view('profile.show', compact('user', 'owner', 'following', 'timeline', 'is_following', 'is_admin'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,9 +33,11 @@ class StatusController extends Controller
|
|||
|
||||
$this->validate($request, [
|
||||
'photo' => 'required|mimes:jpeg,png,bmp,gif|max:' . config('pixelfed.max_photo_size'),
|
||||
'caption' => 'string|max:' . config('pixelfed.max_caption_length')
|
||||
'caption' => 'string|max:' . config('pixelfed.max_caption_length'),
|
||||
'cw' => 'nullable|string'
|
||||
]);
|
||||
|
||||
$cw = $request->filled('cw') && $request->cw == 'on' ? true : false;
|
||||
$monthHash = hash('sha1', date('Y') . date('m'));
|
||||
$userHash = hash('sha1', $user->id . (string) $user->created_at);
|
||||
$storagePath = "public/m/{$monthHash}/{$userHash}";
|
||||
|
@ -45,6 +47,8 @@ class StatusController extends Controller
|
|||
$status = new Status;
|
||||
$status->profile_id = $profile->id;
|
||||
$status->caption = $request->caption;
|
||||
$status->is_nsfw = $cw;
|
||||
|
||||
$status->save();
|
||||
|
||||
$media = new Media;
|
||||
|
|
|
@ -37,7 +37,14 @@ class LikePipeline implements ShouldQueue
|
|||
$status = $this->like->status;
|
||||
$actor = $this->like->actor;
|
||||
|
||||
if($actor->id === $status->profile_id) {
|
||||
$exists = Notification::whereProfileId($status->profile_id)
|
||||
->whereActorId($actor->id)
|
||||
->whereAction('like')
|
||||
->whereItemId($status->id)
|
||||
->whereItemType('App\Status')
|
||||
->count();
|
||||
|
||||
if($actor->id === $status->profile_id || $exists !== 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
72
app/Jobs/MentionPipeline/MentionPipeline.php
Normal file
72
app/Jobs/MentionPipeline/MentionPipeline.php
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\MentionPipeline;
|
||||
|
||||
use Cache, Log, Redis;
|
||||
use App\{Mention, Notification, Profile, Status};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class MentionPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
protected $mention;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Status $status, Mention $mention)
|
||||
{
|
||||
$this->status = $status;
|
||||
$this->mention = $mention;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
|
||||
$status = $this->status;
|
||||
$mention = $this->mention;
|
||||
$actor = $this->status->profile;
|
||||
$target = $this->mention->profile_id;
|
||||
|
||||
$exists = Notification::whereProfileId($target)
|
||||
->whereActorId($actor->id)
|
||||
->whereAction('mention')
|
||||
->whereItemId($status->id)
|
||||
->whereItemType('App\Status')
|
||||
->count();
|
||||
|
||||
if($actor->id === $target || $exists !== 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
$notification = new Notification;
|
||||
$notification->profile_id = $target;
|
||||
$notification->actor_id = $actor->id;
|
||||
$notification->action = 'mention';
|
||||
$notification->message = $mention->toText();
|
||||
$notification->rendered = $mention->toHtml();
|
||||
$notification->item_id = $status->id;
|
||||
$notification->item_type = "App\Status";
|
||||
$notification->save();
|
||||
|
||||
} catch (Exception $e) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
33
app/Mention.php
Normal file
33
app/Mention.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Mention extends Model
|
||||
{
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'profile_id', 'id');
|
||||
}
|
||||
|
||||
public function status()
|
||||
{
|
||||
return $this->belongsTo(Status::class, 'status_id', 'id');
|
||||
}
|
||||
|
||||
public function toText()
|
||||
{
|
||||
$actorName = $this->status->profile->username;
|
||||
return "{$actorName} " . __('notification.mentionedYou');
|
||||
}
|
||||
|
||||
public function toHtml()
|
||||
{
|
||||
$actorName = $this->status->profile->username;
|
||||
$actorUrl = $this->status->profile->url();
|
||||
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> " .
|
||||
__('notification.mentionedYou');
|
||||
}
|
||||
}
|
|
@ -14,6 +14,11 @@ class Profile extends Model
|
|||
|
||||
protected $visible = ['id', 'username', 'name'];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function url($suffix = '')
|
||||
{
|
||||
return url($this->username . $suffix);
|
||||
|
|
|
@ -25,6 +25,9 @@ class Status extends Model
|
|||
|
||||
public function thumb()
|
||||
{
|
||||
if($this->media->count() == 0) {
|
||||
return "data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==";
|
||||
}
|
||||
return url(Storage::url($this->firstMedia()->thumbnail_path));
|
||||
}
|
||||
|
||||
|
|
771
app/Util/Lexer/Autolink.php
Executable file
771
app/Util/Lexer/Autolink.php
Executable file
|
@ -0,0 +1,771 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @author Mike Cochrane <mikec@mikenz.geek.nz>
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @copyright Copyright © 2010, Mike Cochrane, Nick Pope
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
|
||||
namespace App\Util\Lexer;
|
||||
|
||||
use App\Util\Lexer\Regex;
|
||||
use App\Util\Lexer\Extractor;
|
||||
use App\Util\Lexer\StringUtils;
|
||||
|
||||
/**
|
||||
* Twitter Autolink Class
|
||||
*
|
||||
* Parses tweets and generates HTML anchor tags around URLs, usernames,
|
||||
* username/list pairs and hashtags.
|
||||
*
|
||||
* Originally written by {@link http://github.com/mikenz Mike Cochrane}, this
|
||||
* is based on code by {@link http://github.com/mzsanford Matt Sanford} and
|
||||
* heavily modified by {@link http://github.com/ngnpope Nick Pope}.
|
||||
*
|
||||
* @author Mike Cochrane <mikec@mikenz.geek.nz>
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @copyright Copyright © 2010, Mike Cochrane, Nick Pope
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
class Autolink extends Regex
|
||||
{
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked URLs.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $class_url = '';
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked username URLs.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $class_user = 'u-url mention';
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked list URLs.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $class_list = 'u-url list-slug';
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked hashtag URLs.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $class_hash = 'u-url hashtag';
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked cashtag URLs.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $class_cash = 'u-url cashtag';
|
||||
|
||||
/**
|
||||
* URL base for username links (the username without the @ will be appended).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $url_base_user = null;
|
||||
|
||||
/**
|
||||
* URL base for list links (the username/list without the @ will be appended).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $url_base_list = null;
|
||||
|
||||
/**
|
||||
* URL base for hashtag links (the hashtag without the # will be appended).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $url_base_hash = null;
|
||||
|
||||
/**
|
||||
* URL base for cashtag links (the hashtag without the $ will be appended).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $url_base_cash = null;
|
||||
|
||||
/**
|
||||
* Whether to include the value 'nofollow' in the 'rel' attribute.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $nofollow = true;
|
||||
|
||||
/**
|
||||
* Whether to include the value 'noopener' in the 'rel' attribute.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $noopener = true;
|
||||
|
||||
/**
|
||||
* Whether to include the value 'external' in the 'rel' attribute.
|
||||
*
|
||||
* Often this is used to be matched on in JavaScript for dynamically adding
|
||||
* the 'target' attribute which is deprecated in HTML 4.01. In HTML 5 it has
|
||||
* been undeprecated and thus the 'target' attribute can be used. If this is
|
||||
* set to false then the 'target' attribute will be output.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $external = true;
|
||||
|
||||
/**
|
||||
* The scope to open the link in.
|
||||
*
|
||||
* Support for the 'target' attribute was deprecated in HTML 4.01 but has
|
||||
* since been reinstated in HTML 5. To output the 'target' attribute you
|
||||
* must disable the adding of the string 'external' to the 'rel' attribute.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $target = '_blank';
|
||||
|
||||
/**
|
||||
* attribute for invisible span tag
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $invisibleTagAttrs = "style='position:absolute;left:-9999px;'";
|
||||
|
||||
/**
|
||||
*
|
||||
* @var Extractor
|
||||
*/
|
||||
protected $extractor = null;
|
||||
|
||||
/**
|
||||
* Provides fluent method chaining.
|
||||
*
|
||||
* @param string $tweet The tweet to be converted.
|
||||
* @param bool $full_encode Whether to encode all special characters.
|
||||
*
|
||||
* @see __construct()
|
||||
*
|
||||
* @return Autolink
|
||||
*/
|
||||
public static function create($tweet = null, $full_encode = false)
|
||||
{
|
||||
return new static($tweet, $full_encode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads in a tweet to be parsed and converted to contain links.
|
||||
*
|
||||
* As the intent is to produce links and output the modified tweet to the
|
||||
* user, we take this opportunity to ensure that we escape user input.
|
||||
*
|
||||
* @see htmlspecialchars()
|
||||
*
|
||||
* @param string $tweet The tweet to be converted.
|
||||
* @param bool $escape Whether to escape the tweet (default: true).
|
||||
* @param bool $full_encode Whether to encode all special characters.
|
||||
*/
|
||||
public function __construct($tweet = null, $escape = true, $full_encode = false)
|
||||
{
|
||||
if ($escape && !empty($tweet)) {
|
||||
if ($full_encode) {
|
||||
parent::__construct(htmlentities($tweet, ENT_QUOTES, 'UTF-8', false));
|
||||
} else {
|
||||
parent::__construct(htmlspecialchars($tweet, ENT_QUOTES, 'UTF-8', false));
|
||||
}
|
||||
} else {
|
||||
parent::__construct($tweet);
|
||||
}
|
||||
$this->extractor = Extractor::create();
|
||||
$this->url_base_user = config('app.url') . '/';
|
||||
$this->url_base_list = config('app.url') . '/';
|
||||
$this->url_base_hash = config('app.url') . "/discover/tags/";
|
||||
$this->url_base_cash = config('app.url') . '/search?q=%24';
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked URLs.
|
||||
*
|
||||
* @return string CSS class for URL links.
|
||||
*/
|
||||
public function getURLClass()
|
||||
{
|
||||
return $this->class_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked URLs.
|
||||
*
|
||||
* @param string $v CSS class for URL links.
|
||||
*
|
||||
* @return Autolink Fluid method chaining.
|
||||
*/
|
||||
public function setURLClass($v)
|
||||
{
|
||||
$this->class_url = trim($v);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked username URLs.
|
||||
*
|
||||
* @return string CSS class for username links.
|
||||
*/
|
||||
public function getUsernameClass()
|
||||
{
|
||||
return $this->class_user;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked username URLs.
|
||||
*
|
||||
* @param string $v CSS class for username links.
|
||||
*
|
||||
* @return Autolink Fluid method chaining.
|
||||
*/
|
||||
public function setUsernameClass($v)
|
||||
{
|
||||
$this->class_user = trim($v);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked username/list URLs.
|
||||
*
|
||||
* @return string CSS class for username/list links.
|
||||
*/
|
||||
public function getListClass()
|
||||
{
|
||||
return $this->class_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked username/list URLs.
|
||||
*
|
||||
* @param string $v CSS class for username/list links.
|
||||
*
|
||||
* @return Autolink Fluid method chaining.
|
||||
*/
|
||||
public function setListClass($v)
|
||||
{
|
||||
$this->class_list = trim($v);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked hashtag URLs.
|
||||
*
|
||||
* @return string CSS class for hashtag links.
|
||||
*/
|
||||
public function getHashtagClass()
|
||||
{
|
||||
return $this->class_hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked hashtag URLs.
|
||||
*
|
||||
* @param string $v CSS class for hashtag links.
|
||||
*
|
||||
* @return Autolink Fluid method chaining.
|
||||
*/
|
||||
public function setHashtagClass($v)
|
||||
{
|
||||
$this->class_hash = trim($v);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked cashtag URLs.
|
||||
*
|
||||
* @return string CSS class for cashtag links.
|
||||
*/
|
||||
public function getCashtagClass()
|
||||
{
|
||||
return $this->class_cash;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked cashtag URLs.
|
||||
*
|
||||
* @param string $v CSS class for cashtag links.
|
||||
*
|
||||
* @return Autolink Fluid method chaining.
|
||||
*/
|
||||
public function setCashtagClass($v)
|
||||
{
|
||||
$this->class_cash = trim($v);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to include the value 'nofollow' in the 'rel' attribute.
|
||||
*
|
||||
* @return bool Whether to add 'nofollow' to the 'rel' attribute.
|
||||
*/
|
||||
public function getNoFollow()
|
||||
{
|
||||
return $this->nofollow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to include the value 'nofollow' in the 'rel' attribute.
|
||||
*
|
||||
* @param bool $v The value to add to the 'target' attribute.
|
||||
*
|
||||
* @return Autolink Fluid method chaining.
|
||||
*/
|
||||
public function setNoFollow($v)
|
||||
{
|
||||
$this->nofollow = $v;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to include the value 'external' in the 'rel' attribute.
|
||||
*
|
||||
* Often this is used to be matched on in JavaScript for dynamically adding
|
||||
* the 'target' attribute which is deprecated in HTML 4.01. In HTML 5 it has
|
||||
* been undeprecated and thus the 'target' attribute can be used. If this is
|
||||
* set to false then the 'target' attribute will be output.
|
||||
*
|
||||
* @return bool Whether to add 'external' to the 'rel' attribute.
|
||||
*/
|
||||
public function getExternal()
|
||||
{
|
||||
return $this->external;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to include the value 'external' in the 'rel' attribute.
|
||||
*
|
||||
* Often this is used to be matched on in JavaScript for dynamically adding
|
||||
* the 'target' attribute which is deprecated in HTML 4.01. In HTML 5 it has
|
||||
* been undeprecated and thus the 'target' attribute can be used. If this is
|
||||
* set to false then the 'target' attribute will be output.
|
||||
*
|
||||
* @param bool $v The value to add to the 'target' attribute.
|
||||
*
|
||||
* @return Autolink Fluid method chaining.
|
||||
*/
|
||||
public function setExternal($v)
|
||||
{
|
||||
$this->external = $v;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The scope to open the link in.
|
||||
*
|
||||
* Support for the 'target' attribute was deprecated in HTML 4.01 but has
|
||||
* since been reinstated in HTML 5. To output the 'target' attribute you
|
||||
* must disable the adding of the string 'external' to the 'rel' attribute.
|
||||
*
|
||||
* @return string The value to add to the 'target' attribute.
|
||||
*/
|
||||
public function getTarget()
|
||||
{
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
/**
|
||||
* The scope to open the link in.
|
||||
*
|
||||
* Support for the 'target' attribute was deprecated in HTML 4.01 but has
|
||||
* since been reinstated in HTML 5. To output the 'target' attribute you
|
||||
* must disable the adding of the string 'external' to the 'rel' attribute.
|
||||
*
|
||||
* @param string $v The value to add to the 'target' attribute.
|
||||
*
|
||||
* @return Autolink Fluid method chaining.
|
||||
*/
|
||||
public function setTarget($v)
|
||||
{
|
||||
$this->target = trim($v);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Autolink with entities
|
||||
*
|
||||
* @param string $tweet
|
||||
* @param array $entities
|
||||
* @return string
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function autoLinkEntities($tweet = null, $entities = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
|
||||
$text = '';
|
||||
$beginIndex = 0;
|
||||
foreach ($entities as $entity) {
|
||||
if (isset($entity['screen_name'])) {
|
||||
$text .= StringUtils::substr($tweet, $beginIndex, $entity['indices'][0] - $beginIndex + 1);
|
||||
} else {
|
||||
$text .= StringUtils::substr($tweet, $beginIndex, $entity['indices'][0] - $beginIndex);
|
||||
}
|
||||
|
||||
if (isset($entity['url'])) {
|
||||
$text .= $this->linkToUrl($entity);
|
||||
} elseif (isset($entity['hashtag'])) {
|
||||
$text .= $this->linkToHashtag($entity, $tweet);
|
||||
} elseif (isset($entity['screen_name'])) {
|
||||
$text .= $this->linkToMentionAndList($entity);
|
||||
} elseif (isset($entity['cashtag'])) {
|
||||
$text .= $this->linkToCashtag($entity, $tweet);
|
||||
}
|
||||
$beginIndex = $entity['indices'][1];
|
||||
}
|
||||
$text .= StringUtils::substr($tweet, $beginIndex, StringUtils::strlen($tweet));
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link hashtags, URLs, usernames and lists, with JSON entities.
|
||||
*
|
||||
* @param string The tweet to be converted
|
||||
* @param mixed The entities info
|
||||
* @return string that auto-link HTML added
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function autoLinkWithJson($tweet = null, $json = null)
|
||||
{
|
||||
// concatenate entities
|
||||
$entities = array();
|
||||
if (is_object($json)) {
|
||||
$json = $this->object2array($json);
|
||||
}
|
||||
if (is_array($json)) {
|
||||
foreach ($json as $key => $vals) {
|
||||
$entities = array_merge($entities, $json[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// map JSON entity to twitter-text entity
|
||||
foreach ($entities as $idx => $entity) {
|
||||
if (!empty($entity['text'])) {
|
||||
$entities[$idx]['hashtag'] = $entity['text'];
|
||||
}
|
||||
}
|
||||
|
||||
$entities = $this->extractor->removeOverlappingEntities($entities);
|
||||
return $this->autoLinkEntities($tweet, $entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* convert Object to Array
|
||||
*
|
||||
* @param mixed $obj
|
||||
* @return array
|
||||
*/
|
||||
protected function object2array($obj)
|
||||
{
|
||||
$array = (array) $obj;
|
||||
foreach ($array as $key => $var) {
|
||||
if (is_object($var) || is_array($var)) {
|
||||
$array[$key] = $this->object2array($var);
|
||||
}
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link hashtags, URLs, usernames and lists.
|
||||
*
|
||||
* @param string The tweet to be converted
|
||||
* @return string that auto-link HTML added
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function autoLink($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$entities = $this->extractor->extractURLWithoutProtocol(false)->extractEntitiesWithIndices($tweet);
|
||||
return $this->autoLinkEntities($tweet, $entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link the @username and @username/list references in the provided text. Links to @username references will
|
||||
* have the usernameClass CSS classes added. Links to @username/list references will have the listClass CSS class
|
||||
* added.
|
||||
*
|
||||
* @return string that auto-link HTML added
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function autoLinkUsernamesAndLists($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$entities = $this->extractor->extractMentionsOrListsWithIndices($tweet);
|
||||
return $this->autoLinkEntities($tweet, $entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link #hashtag references in the provided Tweet text. The #hashtag links will have the hashtagClass CSS class
|
||||
* added.
|
||||
*
|
||||
* @return string that auto-link HTML added
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function autoLinkHashtags($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$entities = $this->extractor->extractHashtagsWithIndices($tweet);
|
||||
return $this->autoLinkEntities($tweet, $entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link URLs in the Tweet text provided.
|
||||
* <p/>
|
||||
* This only auto-links URLs with protocol.
|
||||
*
|
||||
* @return string that auto-link HTML added
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function autoLinkURLs($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$entities = $this->extractor->extractURLWithoutProtocol(false)->extractURLsWithIndices($tweet);
|
||||
return $this->autoLinkEntities($tweet, $entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link $cashtag references in the provided Tweet text. The $cashtag links will have the cashtagClass CSS class
|
||||
* added.
|
||||
*
|
||||
* @return string that auto-link HTML added
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function autoLinkCashtags($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$entities = $this->extractor->extractCashtagsWithIndices($tweet);
|
||||
return $this->autoLinkEntities($tweet, $entities);
|
||||
}
|
||||
|
||||
public function linkToUrl($entity)
|
||||
{
|
||||
if (!empty($this->class_url)) {
|
||||
$attributes['class'] = $this->class_url;
|
||||
}
|
||||
$attributes['href'] = $entity['url'];
|
||||
$linkText = $this->escapeHTML($entity['url']);
|
||||
|
||||
if (!empty($entity['display_url']) && !empty($entity['expanded_url'])) {
|
||||
// Goal: If a user copies and pastes a tweet containing t.co'ed link, the resulting paste
|
||||
// should contain the full original URL (expanded_url), not the display URL.
|
||||
//
|
||||
// Method: Whenever possible, we actually emit HTML that contains expanded_url, and use
|
||||
// font-size:0 to hide those parts that should not be displayed (because they are not part of display_url).
|
||||
// Elements with font-size:0 get copied even though they are not visible.
|
||||
// Note that display:none doesn't work here. Elements with display:none don't get copied.
|
||||
//
|
||||
// Additionally, we want to *display* ellipses, but we don't want them copied. To make this happen we
|
||||
// wrap the ellipses in a tco-ellipsis class and provide an onCopy handler that sets display:none on
|
||||
// everything with the tco-ellipsis class.
|
||||
//
|
||||
// As an example: The user tweets "hi http://longdomainname.com/foo"
|
||||
// This gets shortened to "hi http://t.co/xyzabc", with display_url = "…nname.com/foo"
|
||||
// This will get rendered as:
|
||||
// <span class='tco-ellipsis'> <!-- This stuff should get displayed but not copied -->
|
||||
// …
|
||||
// <!-- There's a chance the onCopy event handler might not fire. In case that happens,
|
||||
// we include an here so that the … doesn't bump up against the URL and ruin it.
|
||||
// The is inside the tco-ellipsis span so that when the onCopy handler *does*
|
||||
// fire, it doesn't get copied. Otherwise the copied text would have two spaces in a row,
|
||||
// e.g. "hi http://longdomainname.com/foo".
|
||||
// <span style='font-size:0'> </span>
|
||||
// </span>
|
||||
// <span style='font-size:0'> <!-- This stuff should get copied but not displayed -->
|
||||
// http://longdomai
|
||||
// </span>
|
||||
// <span class='js-display-url'> <!-- This stuff should get displayed *and* copied -->
|
||||
// nname.com/foo
|
||||
// </span>
|
||||
// <span class='tco-ellipsis'> <!-- This stuff should get displayed but not copied -->
|
||||
// <span style='font-size:0'> </span>
|
||||
// …
|
||||
// </span>
|
||||
//
|
||||
// Exception: pic.socialhub.dev images, for which expandedUrl = "https://socialhub.dev/#!/username/status/1234/photo/1
|
||||
// For those URLs, display_url is not a substring of expanded_url, so we don't do anything special to render the elided parts.
|
||||
// For a pic.socialhub.dev URL, the only elided part will be the "https://", so this is fine.
|
||||
$displayURL = $entity['display_url'];
|
||||
$expandedURL = $entity['expanded_url'];
|
||||
$displayURLSansEllipses = preg_replace('/…/u', '', $displayURL);
|
||||
$diplayURLIndexInExpandedURL = mb_strpos($expandedURL, $displayURLSansEllipses);
|
||||
|
||||
if ($diplayURLIndexInExpandedURL !== false) {
|
||||
$beforeDisplayURL = mb_substr($expandedURL, 0, $diplayURLIndexInExpandedURL);
|
||||
$afterDisplayURL = mb_substr($expandedURL, $diplayURLIndexInExpandedURL + mb_strlen($displayURLSansEllipses));
|
||||
$precedingEllipsis = (preg_match('/\A…/u', $displayURL)) ? '…' : '';
|
||||
$followingEllipsis = (preg_match('/…\z/u', $displayURL)) ? '…' : '';
|
||||
|
||||
$invisibleSpan = "<span {$this->invisibleTagAttrs}>";
|
||||
|
||||
$linkText = "<span class='tco-ellipsis'>{$precedingEllipsis}{$invisibleSpan} </span></span>";
|
||||
$linkText .= "{$invisibleSpan}{$this->escapeHTML($beforeDisplayURL)}</span>";
|
||||
$linkText .= "<span class='js-display-url'>{$this->escapeHTML($displayURLSansEllipses)}</span>";
|
||||
$linkText .= "{$invisibleSpan}{$this->escapeHTML($afterDisplayURL)}</span>";
|
||||
$linkText .= "<span class='tco-ellipsis'>{$invisibleSpan} </span>{$followingEllipsis}</span>";
|
||||
} else {
|
||||
$linkText = $entity['display_url'];
|
||||
}
|
||||
$attributes['title'] = $entity['expanded_url'];
|
||||
} elseif (!empty($entity['display_url'])) {
|
||||
$linkText = $entity['display_url'];
|
||||
}
|
||||
|
||||
return $this->linkToText($entity, $linkText, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array $entity
|
||||
* @param string $tweet
|
||||
* @return string
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function linkToHashtag($entity, $tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$this->target = false;
|
||||
$attributes = array();
|
||||
$class = array();
|
||||
$hash = StringUtils::substr($tweet, $entity['indices'][0], 1);
|
||||
$linkText = $hash . $entity['hashtag'];
|
||||
|
||||
$attributes['href'] = $this->url_base_hash . $entity['hashtag'] . '?src=hash';
|
||||
$attributes['title'] = '#' . $entity['hashtag'];
|
||||
if (!empty($this->class_hash)) {
|
||||
$class[] = $this->class_hash;
|
||||
}
|
||||
if (preg_match(self::$patterns['rtl_chars'], $linkText)) {
|
||||
$class[] = 'rtl';
|
||||
}
|
||||
if (!empty($class)) {
|
||||
$attributes['class'] = join(' ', $class);
|
||||
}
|
||||
|
||||
return $this->linkToText($entity, $linkText, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array $entity
|
||||
* @return string
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function linkToMentionAndList($entity)
|
||||
{
|
||||
$attributes = array();
|
||||
|
||||
if (!empty($entity['list_slug'])) {
|
||||
# Replace the list and username
|
||||
$linkText = $entity['screen_name'] . $entity['list_slug'];
|
||||
$class = $this->class_list;
|
||||
$url = $this->url_base_list . $linkText;
|
||||
} else {
|
||||
# Replace the username
|
||||
$linkText = $entity['screen_name'];
|
||||
$class = $this->class_user;
|
||||
$url = $this->url_base_user . $linkText;
|
||||
}
|
||||
if (!empty($class)) {
|
||||
$attributes['class'] = $class;
|
||||
}
|
||||
$attributes['href'] = $url;
|
||||
|
||||
return $this->linkToText($entity, $linkText, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array $entity
|
||||
* @param string $tweet
|
||||
* @return string
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function linkToCashtag($entity, $tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$attributes = array();
|
||||
$doller = StringUtils::substr($tweet, $entity['indices'][0], 1);
|
||||
$linkText = $doller . $entity['cashtag'];
|
||||
$attributes['href'] = $this->url_base_cash . $entity['cashtag'];
|
||||
$attributes['title'] = $linkText;
|
||||
if (!empty($this->class_cash)) {
|
||||
$attributes['class'] = $this->class_cash;
|
||||
}
|
||||
|
||||
return $this->linkToText($entity, $linkText, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array $entity
|
||||
* @param string $text
|
||||
* @param array $attributes
|
||||
* @return string
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function linkToText(array $entity, $text, $attributes = array())
|
||||
{
|
||||
$rel = array();
|
||||
if ($this->external) {
|
||||
$rel[] = 'external';
|
||||
}
|
||||
if ($this->nofollow) {
|
||||
$rel[] = 'nofollow';
|
||||
}
|
||||
if ($this->noopener) {
|
||||
$rel[] = 'noopener';
|
||||
}
|
||||
if (!empty($rel)) {
|
||||
$attributes['rel'] = join(' ', $rel);
|
||||
}
|
||||
if ($this->target) {
|
||||
$attributes['target'] = $this->target;
|
||||
}
|
||||
$link = '<a';
|
||||
foreach ($attributes as $key => $val) {
|
||||
$link .= ' ' . $key . '="' . $this->escapeHTML($val) . '"';
|
||||
}
|
||||
$link .= '>' . $text . '</a>';
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* html escape
|
||||
*
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
protected function escapeHTML($text)
|
||||
{
|
||||
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8', false);
|
||||
}
|
||||
}
|
548
app/Util/Lexer/Extractor.php
Executable file
548
app/Util/Lexer/Extractor.php
Executable file
|
@ -0,0 +1,548 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @author Mike Cochrane <mikec@mikenz.geek.nz>
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @copyright Copyright © 2010, Mike Cochrane, Nick Pope
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
|
||||
namespace App\Util\Lexer;
|
||||
|
||||
use App\Util\Lexer\Regex;
|
||||
use App\Util\Lexer\StringUtils;
|
||||
|
||||
/**
|
||||
* Twitter Extractor Class
|
||||
*
|
||||
* Parses tweets and extracts URLs, usernames, username/list pairs and
|
||||
* hashtags.
|
||||
*
|
||||
* Originally written by {@link http://github.com/mikenz Mike Cochrane}, this
|
||||
* is based on code by {@link http://github.com/mzsanford Matt Sanford} and
|
||||
* heavily modified by {@link http://github.com/ngnpope Nick Pope}.
|
||||
*
|
||||
* @author Mike Cochrane <mikec@mikenz.geek.nz>
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @copyright Copyright © 2010, Mike Cochrane, Nick Pope
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
class Extractor extends Regex
|
||||
{
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
protected $extractURLWithoutProtocol = true;
|
||||
|
||||
/**
|
||||
* Provides fluent method chaining.
|
||||
*
|
||||
* @param string $tweet The tweet to be converted.
|
||||
*
|
||||
* @see __construct()
|
||||
*
|
||||
* @return Extractor
|
||||
*/
|
||||
public static function create($tweet = null)
|
||||
{
|
||||
return new self($tweet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads in a tweet to be parsed and extracts elements from it.
|
||||
*
|
||||
* Extracts various parts of a tweet including URLs, usernames, hashtags...
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
*/
|
||||
public function __construct($tweet = null)
|
||||
{
|
||||
parent::__construct($tweet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all parts of a tweet and returns an associative array containing
|
||||
* the extracted elements.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The elements in the tweet.
|
||||
*/
|
||||
public function extract($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
return array(
|
||||
'hashtags' => $this->extractHashtags($tweet),
|
||||
'urls' => $this->extractURLs($tweet),
|
||||
'mentions' => $this->extractMentionedUsernames($tweet),
|
||||
'replyto' => $this->extractRepliedUsernames($tweet),
|
||||
'hashtags_with_indices' => $this->extractHashtagsWithIndices($tweet),
|
||||
'urls_with_indices' => $this->extractURLsWithIndices($tweet),
|
||||
'mentions_with_indices' => $this->extractMentionedUsernamesWithIndices($tweet),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract URLs, @mentions, lists and #hashtag from a given text/tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array list of extracted entities
|
||||
*/
|
||||
public function extractEntitiesWithIndices($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$entities = array();
|
||||
$entities = array_merge($entities, $this->extractURLsWithIndices($tweet));
|
||||
$entities = array_merge($entities, $this->extractHashtagsWithIndices($tweet, false));
|
||||
$entities = array_merge($entities, $this->extractMentionsOrListsWithIndices($tweet));
|
||||
$entities = array_merge($entities, $this->extractCashtagsWithIndices($tweet));
|
||||
$entities = $this->removeOverlappingEntities($entities);
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the hashtags from the tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The hashtag elements in the tweet.
|
||||
*/
|
||||
public function extractHashtags($tweet = null)
|
||||
{
|
||||
$hashtagsOnly = array();
|
||||
$hashtagsWithIndices = $this->extractHashtagsWithIndices($tweet);
|
||||
|
||||
foreach ($hashtagsWithIndices as $hashtagWithIndex) {
|
||||
$hashtagsOnly[] = $hashtagWithIndex['hashtag'];
|
||||
}
|
||||
return $hashtagsOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the cashtags from the tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The cashtag elements in the tweet.
|
||||
*/
|
||||
public function extractCashtags($tweet = null)
|
||||
{
|
||||
$cashtagsOnly = array();
|
||||
$cashtagsWithIndices = $this->extractCashtagsWithIndices($tweet);
|
||||
|
||||
foreach ($cashtagsWithIndices as $cashtagWithIndex) {
|
||||
$cashtagsOnly[] = $cashtagWithIndex['cashtag'];
|
||||
}
|
||||
return $cashtagsOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the URLs from the tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The URL elements in the tweet.
|
||||
*/
|
||||
public function extractURLs($tweet = null)
|
||||
{
|
||||
$urlsOnly = array();
|
||||
$urlsWithIndices = $this->extractURLsWithIndices($tweet);
|
||||
|
||||
foreach ($urlsWithIndices as $urlWithIndex) {
|
||||
$urlsOnly[] = $urlWithIndex['url'];
|
||||
}
|
||||
return $urlsOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all the usernames from the tweet.
|
||||
*
|
||||
* A mention is an occurrence of a username anywhere in a tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The usernames elements in the tweet.
|
||||
*/
|
||||
public function extractMentionedScreennames($tweet = null)
|
||||
{
|
||||
$usernamesOnly = array();
|
||||
$mentionsWithIndices = $this->extractMentionsOrListsWithIndices($tweet);
|
||||
|
||||
foreach ($mentionsWithIndices as $mentionWithIndex) {
|
||||
$screen_name = mb_strtolower($mentionWithIndex['screen_name']);
|
||||
if (empty($screen_name) OR in_array($screen_name, $usernamesOnly)) {
|
||||
continue;
|
||||
}
|
||||
$usernamesOnly[] = $screen_name;
|
||||
}
|
||||
return $usernamesOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all the usernames from the tweet.
|
||||
*
|
||||
* A mention is an occurrence of a username anywhere in a tweet.
|
||||
*
|
||||
* @return array The usernames elements in the tweet.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function extractMentionedUsernames($tweet)
|
||||
{
|
||||
$this->tweet = $tweet;
|
||||
return $this->extractMentionedScreennames($tweet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all the usernames replied to from the tweet.
|
||||
*
|
||||
* A reply is an occurrence of a username at the beginning of a tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The usernames replied to in a tweet.
|
||||
*/
|
||||
public function extractReplyScreenname($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$matched = preg_match(self::$patterns['valid_reply'], $tweet, $matches);
|
||||
# Check username ending in
|
||||
if ($matched && preg_match(self::$patterns['end_mention_match'], $matches[2])) {
|
||||
$matched = false;
|
||||
}
|
||||
return $matched ? $matches[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all the usernames replied to from the tweet.
|
||||
*
|
||||
* A reply is an occurrence of a username at the beginning of a tweet.
|
||||
*
|
||||
* @return array The usernames replied to in a tweet.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function extractRepliedUsernames()
|
||||
{
|
||||
return $this->extractReplyScreenname();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the hashtags and the indices they occur at from the tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @param boolean $checkUrlOverlap if true, check if extracted hashtags overlap URLs and remove overlapping ones
|
||||
* @return array The hashtag elements in the tweet.
|
||||
*/
|
||||
public function extractHashtagsWithIndices($tweet = null, $checkUrlOverlap = true)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
|
||||
if (!preg_match('/[##]/iu', $tweet)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
preg_match_all(self::$patterns['valid_hashtag'], $tweet, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
|
||||
$tags = array();
|
||||
|
||||
foreach ($matches as $match) {
|
||||
list($all, $before, $hash, $hashtag, $outer) = array_pad($match, 3, array('', 0));
|
||||
$start_position = $hash[1] > 0 ? StringUtils::strlen(substr($tweet, 0, $hash[1])) : $hash[1];
|
||||
$end_position = $start_position + StringUtils::strlen($hash[0] . $hashtag[0]);
|
||||
|
||||
if (preg_match(self::$patterns['end_hashtag_match'], $outer[0])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tags[] = array(
|
||||
'hashtag' => $hashtag[0],
|
||||
'indices' => array($start_position, $end_position)
|
||||
);
|
||||
}
|
||||
|
||||
if (!$checkUrlOverlap) {
|
||||
return $tags;
|
||||
}
|
||||
|
||||
# check url overlap
|
||||
$urls = $this->extractURLsWithIndices($tweet);
|
||||
$entities = $this->removeOverlappingEntities(array_merge($tags, $urls));
|
||||
|
||||
$validTags = array();
|
||||
foreach ($entities as $entity) {
|
||||
if (empty($entity['hashtag'])) {
|
||||
continue;
|
||||
}
|
||||
$validTags[] = $entity;
|
||||
}
|
||||
|
||||
return $validTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the cashtags and the indices they occur at from the tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The cashtag elements in the tweet.
|
||||
*/
|
||||
public function extractCashtagsWithIndices($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
|
||||
if (!preg_match('/\$/iu', $tweet)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
preg_match_all(self::$patterns['valid_cashtag'], $tweet, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
|
||||
$tags = array();
|
||||
|
||||
foreach ($matches as $match) {
|
||||
list($all, $before, $dollar, $cash_text, $outer) = array_pad($match, 3, array('', 0));
|
||||
$start_position = $dollar[1] > 0 ? StringUtils::strlen(substr($tweet, 0, $dollar[1])) : $dollar[1];
|
||||
$end_position = $start_position + StringUtils::strlen($dollar[0] . $cash_text[0]);
|
||||
|
||||
if (preg_match(self::$patterns['end_hashtag_match'], $outer[0])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tags[] = array(
|
||||
'cashtag' => $cash_text[0],
|
||||
'indices' => array($start_position, $end_position)
|
||||
);
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the URLs and the indices they occur at from the tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The URLs elements in the tweet.
|
||||
*/
|
||||
public function extractURLsWithIndices($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
|
||||
$needle = $this->extractURLWithoutProtocol() ? '.' : ':';
|
||||
if (strpos($tweet, $needle) === false) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$urls = array();
|
||||
preg_match_all(self::$patterns['valid_url'], $tweet, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
|
||||
|
||||
foreach ($matches as $match) {
|
||||
list($all, $before, $url, $protocol, $domain, $port, $path, $query) = array_pad($match, 8, array(''));
|
||||
$start_position = $url[1] > 0 ? StringUtils::strlen(substr($tweet, 0, $url[1])) : $url[1];
|
||||
$end_position = $start_position + StringUtils::strlen($url[0]);
|
||||
|
||||
$all = $all[0];
|
||||
$before = $before[0];
|
||||
$url = $url[0];
|
||||
$protocol = $protocol[0];
|
||||
$domain = $domain[0];
|
||||
$port = $port[0];
|
||||
$path = $path[0];
|
||||
$query = $query[0];
|
||||
|
||||
// If protocol is missing and domain contains non-ASCII characters,
|
||||
// extract ASCII-only domains.
|
||||
if (empty($protocol)) {
|
||||
if (!$this->extractURLWithoutProtocol || preg_match(self::$patterns['invalid_url_without_protocol_preceding_chars'], $before)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$last_url = null;
|
||||
$ascii_end_position = 0;
|
||||
|
||||
if (preg_match(self::$patterns['valid_ascii_domain'], $domain, $asciiDomain)) {
|
||||
$asciiDomain[0] = preg_replace('/' . preg_quote($domain, '/') . '/u', $asciiDomain[0], $url);
|
||||
$ascii_start_position = StringUtils::strpos($domain, $asciiDomain[0], $ascii_end_position);
|
||||
$ascii_end_position = $ascii_start_position + StringUtils::strlen($asciiDomain[0]);
|
||||
$last_url = array(
|
||||
'url' => $asciiDomain[0],
|
||||
'indices' => array($start_position + $ascii_start_position, $start_position + $ascii_end_position),
|
||||
);
|
||||
if (!empty($path)
|
||||
|| preg_match(self::$patterns['valid_special_short_domain'], $asciiDomain[0])
|
||||
|| !preg_match(self::$patterns['invalid_short_domain'], $asciiDomain[0])) {
|
||||
$urls[] = $last_url;
|
||||
}
|
||||
}
|
||||
|
||||
// no ASCII-only domain found. Skip the entire URL
|
||||
if (empty($last_url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// $last_url only contains domain. Need to add path and query if they exist.
|
||||
if (!empty($path)) {
|
||||
// last_url was not added. Add it to urls here.
|
||||
$last_url['url'] = preg_replace('/' . preg_quote($domain, '/') . '/u', $last_url['url'], $url);
|
||||
$last_url['indices'][1] = $end_position;
|
||||
}
|
||||
} else {
|
||||
// In the case of t.co URLs, don't allow additional path characters
|
||||
if (preg_match(self::$patterns['valid_tco_url'], $url, $tcoUrlMatches)) {
|
||||
$url = $tcoUrlMatches[0];
|
||||
$end_position = $start_position + StringUtils::strlen($url);
|
||||
}
|
||||
$urls[] = array(
|
||||
'url' => $url,
|
||||
'indices' => array($start_position, $end_position),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the usernames and the indices they occur at from the tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The username elements in the tweet.
|
||||
*/
|
||||
public function extractMentionedScreennamesWithIndices($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
|
||||
$usernamesOnly = array();
|
||||
$mentions = $this->extractMentionsOrListsWithIndices($tweet);
|
||||
foreach ($mentions as $mention) {
|
||||
if (isset($mention['list_slug'])) {
|
||||
unset($mention['list_slug']);
|
||||
}
|
||||
$usernamesOnly[] = $mention;
|
||||
}
|
||||
return $usernamesOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the usernames and the indices they occur at from the tweet.
|
||||
*
|
||||
* @return array The username elements in the tweet.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function extractMentionedUsernamesWithIndices()
|
||||
{
|
||||
return $this->extractMentionedScreennamesWithIndices();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the usernames and the indices they occur at from the tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The username elements in the tweet.
|
||||
*/
|
||||
public function extractMentionsOrListsWithIndices($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
|
||||
if (!preg_match('/[@@]/iu', $tweet)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
preg_match_all(self::$patterns['valid_mentions_or_lists'], $tweet, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
|
||||
$results = array();
|
||||
|
||||
foreach ($matches as $match) {
|
||||
list($all, $before, $at, $username, $list_slug, $outer) = array_pad($match, 6, array('', 0));
|
||||
$start_position = $at[1] > 0 ? StringUtils::strlen(substr($tweet, 0, $at[1])) : $at[1];
|
||||
$end_position = $start_position + StringUtils::strlen($at[0]) + StringUtils::strlen($username[0]);
|
||||
$entity = array(
|
||||
'screen_name' => $username[0],
|
||||
'list_slug' => $list_slug[0],
|
||||
'indices' => array($start_position, $end_position),
|
||||
);
|
||||
|
||||
if (preg_match(self::$patterns['end_mention_match'], $outer[0])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!empty($list_slug[0])) {
|
||||
$entity['indices'][1] = $end_position + StringUtils::strlen($list_slug[0]);
|
||||
}
|
||||
|
||||
$results[] = $entity;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the usernames and the indices they occur at from the tweet.
|
||||
*
|
||||
* @return array The username elements in the tweet.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function extractMentionedUsernamesOrListsWithIndices()
|
||||
{
|
||||
return $this->extractMentionsOrListsWithIndices();
|
||||
}
|
||||
|
||||
/**
|
||||
* setter/getter for extractURLWithoutProtocol
|
||||
*
|
||||
* @param boolean $flag
|
||||
* @return Extractor
|
||||
*/
|
||||
public function extractURLWithoutProtocol($flag = null)
|
||||
{
|
||||
if (is_null($flag)) {
|
||||
return $this->extractURLWithoutProtocol;
|
||||
}
|
||||
$this->extractURLWithoutProtocol = (bool) $flag;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove overlapping entities.
|
||||
* This returns a new array with no overlapping entities.
|
||||
*
|
||||
* @param array $entities
|
||||
* @return array
|
||||
*/
|
||||
public function removeOverlappingEntities($entities)
|
||||
{
|
||||
$result = array();
|
||||
usort($entities, array($this, 'sortEntites'));
|
||||
|
||||
$prev = null;
|
||||
foreach ($entities as $entity) {
|
||||
if (isset($prev) && $entity['indices'][0] < $prev['indices'][1]) {
|
||||
continue;
|
||||
}
|
||||
$prev = $entity;
|
||||
$result[] = $entity;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* sort by entity start index
|
||||
*
|
||||
* @param array $a
|
||||
* @param array $b
|
||||
* @return int
|
||||
*/
|
||||
protected function sortEntites($a, $b)
|
||||
{
|
||||
if ($a['indices'][0] == $b['indices'][0]) {
|
||||
return 0;
|
||||
}
|
||||
return ($a['indices'][0] < $b['indices'][0]) ? -1 : 1;
|
||||
}
|
||||
}
|
202
app/Util/Lexer/HitHighlighter.php
Executable file
202
app/Util/Lexer/HitHighlighter.php
Executable file
|
@ -0,0 +1,202 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @copyright Copyright © 2010, Nick Pope
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
|
||||
namespace App\Util\Lexer;
|
||||
|
||||
use App\Util\Lexer\Regex;
|
||||
use App\Util\Lexer\StringUtils;
|
||||
|
||||
/**
|
||||
* Twitter HitHighlighter Class
|
||||
*
|
||||
* Performs "hit highlighting" on tweets that have been auto-linked already.
|
||||
* Useful with the results returned from the search API.
|
||||
*
|
||||
* Originally written by {@link http://github.com/mikenz Mike Cochrane}, this
|
||||
* is based on code by {@link http://github.com/mzsanford Matt Sanford} and
|
||||
* heavily modified by {@link http://github.com/ngnpope Nick Pope}.
|
||||
*
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @copyright Copyright © 2010, Nick Pope
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
class HitHighlighter extends Regex
|
||||
{
|
||||
|
||||
/**
|
||||
* The tag to surround hits with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tag = 'em';
|
||||
|
||||
/**
|
||||
* Provides fluent method chaining.
|
||||
*
|
||||
* @param string $tweet The tweet to be hit highlighted.
|
||||
* @param bool $full_encode Whether to encode all special characters.
|
||||
*
|
||||
* @see __construct()
|
||||
*
|
||||
* @return HitHighlighter
|
||||
*/
|
||||
public static function create($tweet = null, $full_encode = false)
|
||||
{
|
||||
return new self($tweet, $full_encode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads in a tweet to be parsed and hit highlighted.
|
||||
*
|
||||
* We take this opportunity to ensure that we escape user input.
|
||||
*
|
||||
* @see htmlspecialchars()
|
||||
*
|
||||
* @param string $tweet The tweet to be hit highlighted.
|
||||
* @param bool $escape Whether to escape the tweet (default: true).
|
||||
* @param bool $full_encode Whether to encode all special characters.
|
||||
*/
|
||||
public function __construct($tweet = null, $escape = true, $full_encode = false)
|
||||
{
|
||||
if (!empty($tweet) && $escape) {
|
||||
if ($full_encode) {
|
||||
parent::__construct(htmlentities($tweet, ENT_QUOTES, 'UTF-8', false));
|
||||
} else {
|
||||
parent::__construct(htmlspecialchars($tweet, ENT_QUOTES, 'UTF-8', false));
|
||||
}
|
||||
} else {
|
||||
parent::__construct($tweet);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the highlighting tag to surround hits with. The default tag is 'em'.
|
||||
*
|
||||
* @return string The tag name.
|
||||
*/
|
||||
public function getTag()
|
||||
{
|
||||
return $this->tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the highlighting tag to surround hits with. The default tag is 'em'.
|
||||
*
|
||||
* @param string $v The tag name.
|
||||
*
|
||||
* @return HitHighlighter Fluid method chaining.
|
||||
*/
|
||||
public function setTag($v)
|
||||
{
|
||||
$this->tag = $v;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hit highlights the tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to be hit highlighted.
|
||||
* @param array $hits An array containing the start and end index pairs
|
||||
* for the highlighting.
|
||||
* @param bool $escape Whether to escape the tweet (default: true).
|
||||
* @param bool $full_encode Whether to encode all special characters.
|
||||
*
|
||||
* @return string The hit highlighted tweet.
|
||||
*/
|
||||
public function highlight($tweet = null, array $hits = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
if (empty($hits)) {
|
||||
return $tweet;
|
||||
}
|
||||
$highlightTweet = '';
|
||||
$tags = array('<' . $this->tag . '>', '</' . $this->tag . '>');
|
||||
# Check whether we can simply replace or whether we need to chunk...
|
||||
if (strpos($tweet, '<') === false) {
|
||||
$ti = 0; // tag increment (for added tags)
|
||||
$highlightTweet = $tweet;
|
||||
foreach ($hits as $hit) {
|
||||
$highlightTweet = StringUtils::substrReplace($highlightTweet, $tags[0], $hit[0] + $ti, 0);
|
||||
$ti += StringUtils::strlen($tags[0]);
|
||||
$highlightTweet = StringUtils::substrReplace($highlightTweet, $tags[1], $hit[1] + $ti, 0);
|
||||
$ti += StringUtils::strlen($tags[1]);
|
||||
}
|
||||
} else {
|
||||
$chunks = preg_split('/[<>]/iu', $tweet);
|
||||
$chunk = $chunks[0];
|
||||
$chunk_index = 0;
|
||||
$chunk_cursor = 0;
|
||||
$offset = 0;
|
||||
$start_in_chunk = false;
|
||||
# Flatten the multidimensional hits array:
|
||||
$hits_flat = array();
|
||||
foreach ($hits as $hit) {
|
||||
$hits_flat = array_merge($hits_flat, $hit);
|
||||
}
|
||||
# Loop over the hit indices:
|
||||
for ($index = 0; $index < count($hits_flat); $index++) {
|
||||
$hit = $hits_flat[$index];
|
||||
$tag = $tags[$index % 2];
|
||||
$placed = false;
|
||||
while ($chunk !== null && $hit >= ($i = $offset + StringUtils::strlen($chunk))) {
|
||||
$highlightTweet .= StringUtils::substr($chunk, $chunk_cursor);
|
||||
if ($start_in_chunk && $hit === $i) {
|
||||
$highlightTweet .= $tag;
|
||||
$placed = true;
|
||||
}
|
||||
if (isset($chunks[$chunk_index + 1])) {
|
||||
$highlightTweet .= '<' . $chunks[$chunk_index + 1] . '>';
|
||||
}
|
||||
$offset += StringUtils::strlen($chunk);
|
||||
$chunk_cursor = 0;
|
||||
$chunk_index += 2;
|
||||
$chunk = (isset($chunks[$chunk_index]) ? $chunks[$chunk_index] : null);
|
||||
$start_in_chunk = false;
|
||||
}
|
||||
if (!$placed && $chunk !== null) {
|
||||
$hit_spot = $hit - $offset;
|
||||
$highlightTweet .= StringUtils::substr($chunk, $chunk_cursor, $hit_spot - $chunk_cursor) . $tag;
|
||||
$chunk_cursor = $hit_spot;
|
||||
$start_in_chunk = ($index % 2 === 0);
|
||||
$placed = true;
|
||||
}
|
||||
# Ultimate fallback - hits that run off the end get a closing tag:
|
||||
if (!$placed) {
|
||||
$highlightTweet .= $tag;
|
||||
}
|
||||
}
|
||||
if ($chunk !== null) {
|
||||
if ($chunk_cursor < StringUtils::strlen($chunk)) {
|
||||
$highlightTweet .= StringUtils::substr($chunk, $chunk_cursor);
|
||||
}
|
||||
for ($index = $chunk_index + 1; $index < count($chunks); $index++) {
|
||||
$highlightTweet .= ($index % 2 === 0 ? $chunks[$index] : '<' . $chunks[$index] . '>');
|
||||
}
|
||||
}
|
||||
}
|
||||
return $highlightTweet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hit highlights the tweet.
|
||||
*
|
||||
* @param array $hits An array containing the start and end index pairs
|
||||
* for the highlighting.
|
||||
*
|
||||
* @return string The hit highlighted tweet.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function addHitHighlighting(array $hits)
|
||||
{
|
||||
return $this->highlight($this->tweet, $hits);
|
||||
}
|
||||
}
|
348
app/Util/Lexer/LooseAutolink.php
Executable file
348
app/Util/Lexer/LooseAutolink.php
Executable file
|
@ -0,0 +1,348 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @author Mike Cochrane <mikec@mikenz.geek.nz>
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @author Takashi Nojima
|
||||
* @copyright Copyright 2014 Mike Cochrane, Nick Pope, Takashi Nojima
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
|
||||
namespace App\Util\Lexer;
|
||||
|
||||
use App\Util\Lexer\Autolink;
|
||||
|
||||
/**
|
||||
* Twitter LooseAutolink Class
|
||||
*
|
||||
* Parses tweets and generates HTML anchor tags around URLs, usernames,
|
||||
* username/list pairs and hashtags.
|
||||
*
|
||||
* Originally written by {@link http://github.com/mikenz Mike Cochrane}, this
|
||||
* is based on code by {@link http://github.com/mzsanford Matt Sanford} and
|
||||
* heavily modified by {@link http://github.com/ngnpope Nick Pope}.
|
||||
*
|
||||
* @author Mike Cochrane <mikec@mikenz.geek.nz>
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @author Takashi Nojima
|
||||
* @copyright Copyright 2014 Mike Cochrane, Nick Pope, Takashi Nojima
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
* @since 1.8.0
|
||||
* @deprecated since version 1.9.0
|
||||
*/
|
||||
class LooseAutolink extends Autolink
|
||||
{
|
||||
|
||||
/**
|
||||
* Auto-link hashtags, URLs, usernames and lists.
|
||||
*
|
||||
* @param string The tweet to be converted
|
||||
* @return string that auto-link HTML added
|
||||
* @deprecated since version 1.9.0
|
||||
*/
|
||||
public function autoLink($tweet = null)
|
||||
{
|
||||
if (!is_null($tweet)) {
|
||||
$this->tweet = $tweet;
|
||||
}
|
||||
return $this->addLinks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link the @username and @username/list references in the provided text. Links to @username references will
|
||||
* have the usernameClass CSS classes added. Links to @username/list references will have the listClass CSS class
|
||||
* added.
|
||||
*
|
||||
* @return string that auto-link HTML added
|
||||
*/
|
||||
public function autoLinkUsernamesAndLists($tweet = null)
|
||||
{
|
||||
if (!is_null($tweet)) {
|
||||
$this->tweet = $tweet;
|
||||
}
|
||||
return $this->addLinksToUsernamesAndLists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link #hashtag references in the provided Tweet text. The #hashtag links will have the hashtagClass CSS class
|
||||
* added.
|
||||
*
|
||||
* @return string that auto-link HTML added
|
||||
*/
|
||||
public function autoLinkHashtags($tweet = null)
|
||||
{
|
||||
if (!is_null($tweet)) {
|
||||
$this->tweet = $tweet;
|
||||
}
|
||||
return $this->addLinksToHashtags();
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link URLs in the Tweet text provided.
|
||||
* <p/>
|
||||
* This only auto-links URLs with protocol.
|
||||
*
|
||||
* @return string that auto-link HTML added
|
||||
*/
|
||||
public function autoLinkURLs($tweet = null)
|
||||
{
|
||||
if (!is_null($tweet)) {
|
||||
$this->tweet = $tweet;
|
||||
}
|
||||
return $this->addLinksToURLs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link $cashtag references in the provided Tweet text. The $cashtag links will have the cashtagClass CSS class
|
||||
* added.
|
||||
*
|
||||
* @return string that auto-link HTML added
|
||||
*/
|
||||
public function autoLinkCashtags($tweet = null)
|
||||
{
|
||||
if (!is_null($tweet)) {
|
||||
$this->tweet = $tweet;
|
||||
}
|
||||
return $this->addLinksToCashtags();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds links to all elements in the tweet.
|
||||
*
|
||||
* @return string The modified tweet.
|
||||
* @deprecated since version 1.9.0
|
||||
*/
|
||||
public function addLinks()
|
||||
{
|
||||
$original = $this->tweet;
|
||||
$this->tweet = $this->addLinksToURLs();
|
||||
$this->tweet = $this->addLinksToHashtags();
|
||||
$this->tweet = $this->addLinksToCashtags();
|
||||
$this->tweet = $this->addLinksToUsernamesAndLists();
|
||||
$modified = $this->tweet;
|
||||
$this->tweet = $original;
|
||||
return $modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds links to hashtag elements in the tweet.
|
||||
*
|
||||
* @return string The modified tweet.
|
||||
*/
|
||||
public function addLinksToHashtags()
|
||||
{
|
||||
return preg_replace_callback(
|
||||
self::$patterns['valid_hashtag'],
|
||||
array($this, '_addLinksToHashtags'),
|
||||
$this->tweet
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds links to cashtag elements in the tweet.
|
||||
*
|
||||
* @return string The modified tweet.
|
||||
*/
|
||||
public function addLinksToCashtags()
|
||||
{
|
||||
return preg_replace_callback(
|
||||
self::$patterns['valid_cashtag'],
|
||||
array($this, '_addLinksToCashtags'),
|
||||
$this->tweet
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds links to URL elements in the tweet.
|
||||
*
|
||||
* @return string The modified tweet
|
||||
*/
|
||||
public function addLinksToURLs()
|
||||
{
|
||||
return preg_replace_callback(self::$patterns['valid_url'], array($this, '_addLinksToURLs'), $this->tweet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds links to username/list elements in the tweet.
|
||||
*
|
||||
* @return string The modified tweet.
|
||||
*/
|
||||
public function addLinksToUsernamesAndLists()
|
||||
{
|
||||
return preg_replace_callback(
|
||||
self::$patterns['valid_mentions_or_lists'],
|
||||
array($this, '_addLinksToUsernamesAndLists'),
|
||||
$this->tweet
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a tweet element in an HTML anchor tag using the provided URL.
|
||||
*
|
||||
* This is a helper function to perform the generation of the link.
|
||||
*
|
||||
* @param string $url The URL to use as the href.
|
||||
* @param string $class The CSS class(es) to apply (space separated).
|
||||
* @param string $element The tweet element to wrap.
|
||||
*
|
||||
* @return string The tweet element with a link applied.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
protected function wrap($url, $class, $element)
|
||||
{
|
||||
$link = '<a';
|
||||
if ($class) {
|
||||
$link .= ' class="' . $class . '"';
|
||||
}
|
||||
$link .= ' href="' . $url . '"';
|
||||
$rel = array();
|
||||
if ($this->external) {
|
||||
$rel[] = 'external';
|
||||
}
|
||||
if ($this->nofollow) {
|
||||
$rel[] = 'nofollow';
|
||||
}
|
||||
if (!empty($rel)) {
|
||||
$link .= ' rel="' . implode(' ', $rel) . '"';
|
||||
}
|
||||
if ($this->target) {
|
||||
$link .= ' target="' . $this->target . '"';
|
||||
}
|
||||
$link .= '>' . $element . '</a>';
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a tweet element in an HTML anchor tag using the provided URL.
|
||||
*
|
||||
* This is a helper function to perform the generation of the hashtag link.
|
||||
*
|
||||
* @param string $url The URL to use as the href.
|
||||
* @param string $class The CSS class(es) to apply (space separated).
|
||||
* @param string $element The tweet element to wrap.
|
||||
*
|
||||
* @return string The tweet element with a link applied.
|
||||
*/
|
||||
protected function wrapHash($url, $class, $element)
|
||||
{
|
||||
$title = preg_replace('/#/u', '#', $element);
|
||||
$link = '<a';
|
||||
$link .= ' href="' . $url . '"';
|
||||
$link .= ' title="' . $title . '"';
|
||||
if ($class) {
|
||||
$link .= ' class="' . $class . '"';
|
||||
}
|
||||
$rel = array();
|
||||
if ($this->external) {
|
||||
$rel[] = 'external';
|
||||
}
|
||||
if ($this->nofollow) {
|
||||
$rel[] = 'nofollow';
|
||||
}
|
||||
if (!empty($rel)) {
|
||||
$link .= ' rel="' . implode(' ', $rel) . '"';
|
||||
}
|
||||
if ($this->target) {
|
||||
$link .= ' target="' . $this->target . '"';
|
||||
}
|
||||
$link .= '>' . $element . '</a>';
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used by the method that adds links to hashtags.
|
||||
*
|
||||
* @see addLinksToHashtags()
|
||||
* @param array $matches The regular expression matches.
|
||||
* @return string The link-wrapped hashtag.
|
||||
*/
|
||||
protected function _addLinksToHashtags($matches)
|
||||
{
|
||||
list($all, $before, $hash, $tag, $after) = array_pad($matches, 5, '');
|
||||
if (preg_match(self::$patterns['end_hashtag_match'], $after)
|
||||
|| (!preg_match('!\A["\']!', $before) && preg_match('!\A["\']!', $after)) || preg_match('!\A</!', $after)) {
|
||||
return $all;
|
||||
}
|
||||
$replacement = $before;
|
||||
$element = $hash . $tag;
|
||||
$url = $this->url_base_hash . $tag;
|
||||
$class_hash = $this->class_hash;
|
||||
if (preg_match(self::$patterns['rtl_chars'], $element)) {
|
||||
$class_hash .= ' rtl';
|
||||
}
|
||||
$replacement .= $this->wrapHash($url, $class_hash, $element);
|
||||
return $replacement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used by the method that adds links to cashtags.
|
||||
*
|
||||
* @see addLinksToCashtags()
|
||||
* @param array $matches The regular expression matches.
|
||||
* @return string The link-wrapped cashtag.
|
||||
*/
|
||||
protected function _addLinksToCashtags($matches)
|
||||
{
|
||||
list($all, $before, $cash, $tag, $after) = array_pad($matches, 5, '');
|
||||
if (preg_match(self::$patterns['end_cashtag_match'], $after)
|
||||
|| (!preg_match('!\A["\']!', $before) && preg_match('!\A["\']!', $after)) || preg_match('!\A</!', $after)) {
|
||||
return $all;
|
||||
}
|
||||
$replacement = $before;
|
||||
$element = $cash . $tag;
|
||||
$url = $this->url_base_cash . $tag;
|
||||
$replacement .= $this->wrapHash($url, $this->class_cash, $element);
|
||||
return $replacement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used by the method that adds links to URLs.
|
||||
*
|
||||
* @see addLinksToURLs()
|
||||
* @param array $matches The regular expression matches.
|
||||
* @return string The link-wrapped URL.
|
||||
*/
|
||||
protected function _addLinksToURLs($matches)
|
||||
{
|
||||
list($all, $before, $url, $protocol, $domain, $path, $query) = array_pad($matches, 7, '');
|
||||
$url = htmlspecialchars($url, ENT_QUOTES, 'UTF-8', false);
|
||||
if (!$protocol) {
|
||||
return $all;
|
||||
}
|
||||
return $before . $this->wrap($url, $this->class_url, $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used by the method that adds links to username/list pairs.
|
||||
*
|
||||
* @see addLinksToUsernamesAndLists()
|
||||
* @param array $matches The regular expression matches.
|
||||
* @return string The link-wrapped username/list pair.
|
||||
*/
|
||||
protected function _addLinksToUsernamesAndLists($matches)
|
||||
{
|
||||
list($all, $before, $at, $username, $slash_listname, $after) = array_pad($matches, 6, '');
|
||||
# If $after is not empty, there is an invalid character.
|
||||
if (!empty($slash_listname)) {
|
||||
# Replace the list and username
|
||||
$element = $username . $slash_listname;
|
||||
$class = $this->class_list;
|
||||
$url = $this->url_base_list . $element;
|
||||
} else {
|
||||
if (preg_match(self::$patterns['end_mention_match'], $after)) {
|
||||
return $all;
|
||||
}
|
||||
# Replace the username
|
||||
$element = $username;
|
||||
$class = $this->class_user;
|
||||
$url = $this->url_base_user . $element;
|
||||
}
|
||||
# XXX: Due to use of preg_replace_callback() for multiple replacements in a
|
||||
# single tweet and also as only the match is replaced and we have to
|
||||
# use a look-ahead for $after because there is no equivalent for the
|
||||
# $' (dollar apostrophe) global from Ruby, we MUST NOT append $after.
|
||||
return $before . $at . $this->wrap($url, $class, $element);
|
||||
}
|
||||
}
|
337
app/Util/Lexer/Regex.php
Executable file
337
app/Util/Lexer/Regex.php
Executable file
File diff suppressed because one or more lines are too long
104
app/Util/Lexer/StringUtils.php
Executable file
104
app/Util/Lexer/StringUtils.php
Executable file
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @author Takashi Nojima
|
||||
* @copyright Copyright 2014, Takashi Nojima
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
|
||||
namespace App\Util\Lexer;
|
||||
|
||||
/**
|
||||
* String utility
|
||||
*
|
||||
* @author Takashi Nojima
|
||||
* @copyright Copyright 2014, Takashi Nojima
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter
|
||||
*/
|
||||
class StringUtils
|
||||
{
|
||||
|
||||
/**
|
||||
* alias of mb_substr
|
||||
*
|
||||
* @param string $str
|
||||
* @param integer $start
|
||||
* @param integer $length
|
||||
* @param string $encoding
|
||||
* @return string
|
||||
*/
|
||||
public static function substr($str, $start, $length = null, $encoding = 'UTF-8')
|
||||
{
|
||||
if (is_null($length)) {
|
||||
// for PHP <= 5.4.7
|
||||
$length = mb_strlen($str, $encoding);
|
||||
}
|
||||
return mb_substr($str, $start, $length, $encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* alias of mb_strlen
|
||||
*
|
||||
* @param string $str
|
||||
* @param string $encoding
|
||||
* @return integer
|
||||
*/
|
||||
public static function strlen($str, $encoding = 'UTF-8')
|
||||
{
|
||||
return mb_strlen($str, $encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* alias of mb_strpos
|
||||
*
|
||||
* @param string $haystack
|
||||
* @param string $needle
|
||||
* @param integer $offset
|
||||
* @param string $encoding
|
||||
* @return integer
|
||||
*/
|
||||
public static function strpos($haystack, $needle, $offset = 0, $encoding = 'UTF-8')
|
||||
{
|
||||
return mb_strpos($haystack, $needle, $offset, $encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* A multibyte-aware substring replacement function.
|
||||
*
|
||||
* @param string $string The string to modify.
|
||||
* @param string $replacement The replacement string.
|
||||
* @param int $start The start of the replacement.
|
||||
* @param int $length The number of characters to replace.
|
||||
* @param string $encoding The encoding of the string.
|
||||
*
|
||||
* @return string The modified string.
|
||||
*
|
||||
* @see http://www.php.net/manual/en/function.substr-replace.php#90146
|
||||
*/
|
||||
public static function substrReplace($string, $replacement, $start, $length = null, $encoding = 'UTF-8')
|
||||
{
|
||||
if (extension_loaded('mbstring') === true) {
|
||||
$string_length = static::strlen($string, $encoding);
|
||||
if ($start < 0) {
|
||||
$start = max(0, $string_length + $start);
|
||||
} elseif ($start > $string_length) {
|
||||
$start = $string_length;
|
||||
}
|
||||
if ($length < 0) {
|
||||
$length = max(0, $string_length - $start + $length);
|
||||
} elseif ((is_null($length) === true) || ($length > $string_length)) {
|
||||
$length = $string_length;
|
||||
}
|
||||
if (($start + $length) > $string_length) {
|
||||
$length = $string_length - $start;
|
||||
}
|
||||
|
||||
$suffixOffset = $start + $length;
|
||||
$suffixLength = $string_length - $start - $length;
|
||||
return static::substr($string, 0, $start, $encoding) . $replacement . static::substr($string, $suffixOffset, $suffixLength, $encoding);
|
||||
}
|
||||
return (is_null($length) === true) ? substr_replace($string, $replacement, $start) : substr_replace($string, $replacement, $start, $length);
|
||||
}
|
||||
}
|
388
app/Util/Lexer/Validator.php
Executable file
388
app/Util/Lexer/Validator.php
Executable file
|
@ -0,0 +1,388 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @copyright Copyright © 2010, Nick Pope
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
|
||||
namespace App\Util\Lexer;
|
||||
|
||||
use App\Util\Lexer\Regex;
|
||||
use App\Util\Lexer\Extractor;
|
||||
use App\Util\Lexer\StringUtils;
|
||||
|
||||
/**
|
||||
* Twitter Validator Class
|
||||
*
|
||||
* Performs "validation" on tweets.
|
||||
*
|
||||
* Originally written by {@link http://github.com/mikenz Mike Cochrane}, this
|
||||
* is based on code by {@link http://github.com/mzsanford Matt Sanford} and
|
||||
* heavily modified by {@link http://github.com/ngnpope Nick Pope}.
|
||||
*
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @copyright Copyright © 2010, Nick Pope
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
class Validator extends Regex
|
||||
{
|
||||
|
||||
/**
|
||||
* The maximum length of a tweet.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const MAX_LENGTH = 140;
|
||||
|
||||
/**
|
||||
* The length of a short URL beginning with http:
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $short_url_length = 23;
|
||||
|
||||
/**
|
||||
* The length of a short URL beginning with http:
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $short_url_length_https = 23;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var Extractor
|
||||
*/
|
||||
protected $extractor = null;
|
||||
|
||||
/**
|
||||
* Provides fluent method chaining.
|
||||
*
|
||||
* @param string $tweet The tweet to be validated.
|
||||
* @param mixed $config Setup short URL length from Twitter API /help/configuration response.
|
||||
*
|
||||
* @see __construct()
|
||||
*
|
||||
* @return Validator
|
||||
*/
|
||||
public static function create($tweet = null, $config = null)
|
||||
{
|
||||
return new self($tweet, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads in a tweet to be parsed and validates it.
|
||||
*
|
||||
* @param string $tweet The tweet to validate.
|
||||
*/
|
||||
public function __construct($tweet = null, $config = null)
|
||||
{
|
||||
parent::__construct($tweet);
|
||||
if (!empty($config)) {
|
||||
$this->setConfiguration($config);
|
||||
}
|
||||
$this->extractor = Extractor::create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup short URL length from Twitter API /help/configuration response
|
||||
*
|
||||
* @param mixed $config
|
||||
* @return Validator
|
||||
* @link https://dev.twitter.com/docs/api/1/get/help/configuration
|
||||
*/
|
||||
public function setConfiguration($config)
|
||||
{
|
||||
if (is_array($config)) {
|
||||
// setup from array
|
||||
if (isset($config['short_url_length'])) {
|
||||
$this->setShortUrlLength($config['short_url_length']);
|
||||
}
|
||||
if (isset($config['short_url_length_https'])) {
|
||||
$this->setShortUrlLengthHttps($config['short_url_length_https']);
|
||||
}
|
||||
} elseif (is_object($config)) {
|
||||
// setup from object
|
||||
if (isset($config->short_url_length)) {
|
||||
$this->setShortUrlLength($config->short_url_length);
|
||||
}
|
||||
if (isset($config->short_url_length_https)) {
|
||||
$this->setShortUrlLengthHttps($config->short_url_length_https);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the length of a short URL beginning with http:
|
||||
*
|
||||
* @param mixed $length
|
||||
* @return Validator
|
||||
*/
|
||||
public function setShortUrlLength($length)
|
||||
{
|
||||
$this->short_url_length = intval($length);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the length of a short URL beginning with http:
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getShortUrlLength()
|
||||
{
|
||||
return $this->short_url_length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the length of a short URL beginning with https:
|
||||
*
|
||||
* @param mixed $length
|
||||
* @return Validator
|
||||
*/
|
||||
public function setShortUrlLengthHttps($length)
|
||||
{
|
||||
$this->short_url_length_https = intval($length);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the length of a short URL beginning with https:
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getShortUrlLengthHttps()
|
||||
{
|
||||
return $this->short_url_length_https;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a tweet is valid.
|
||||
*
|
||||
* @param string $tweet The tweet to validate.
|
||||
* @return boolean Whether the tweet is valid.
|
||||
*/
|
||||
public function isValidTweetText($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$length = $this->getTweetLength($tweet);
|
||||
if (!$tweet || !$length) {
|
||||
return false;
|
||||
}
|
||||
if ($length > self::MAX_LENGTH) {
|
||||
return false;
|
||||
}
|
||||
if (preg_match(self::$patterns['invalid_characters'], $tweet)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a tweet is valid.
|
||||
*
|
||||
* @return boolean Whether the tweet is valid.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function validateTweet()
|
||||
{
|
||||
return $this->isValidTweetText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a username is valid.
|
||||
*
|
||||
* @param string $username The username to validate.
|
||||
* @return boolean Whether the username is valid.
|
||||
*/
|
||||
public function isValidUsername($username = null)
|
||||
{
|
||||
if (is_null($username)) {
|
||||
$username = $this->tweet;
|
||||
}
|
||||
$length = StringUtils::strlen($username);
|
||||
if (empty($username) || !$length) {
|
||||
return false;
|
||||
}
|
||||
$extracted = $this->extractor->extractMentionedScreennames($username);
|
||||
return count($extracted) === 1 && $extracted[0] === substr($username, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a username is valid.
|
||||
*
|
||||
* @return boolean Whether the username is valid.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function validateUsername()
|
||||
{
|
||||
return $this->isValidUsername();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a list is valid.
|
||||
*
|
||||
* @param string $list The list name to validate.
|
||||
* @return boolean Whether the list is valid.
|
||||
*/
|
||||
public function isValidList($list = null)
|
||||
{
|
||||
if (is_null($list)) {
|
||||
$list = $this->tweet;
|
||||
}
|
||||
$length = StringUtils::strlen($list);
|
||||
if (empty($list) || !$length) {
|
||||
return false;
|
||||
}
|
||||
preg_match(self::$patterns['valid_mentions_or_lists'], $list, $matches);
|
||||
$matches = array_pad($matches, 5, '');
|
||||
return isset($matches) && $matches[1] === '' && $matches[4] && !empty($matches[4]) && $matches[5] === '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a list is valid.
|
||||
*
|
||||
* @return boolean Whether the list is valid.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function validateList()
|
||||
{
|
||||
return $this->isValidList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a hashtag is valid.
|
||||
*
|
||||
* @param string $hashtag The hashtag to validate.
|
||||
* @return boolean Whether the hashtag is valid.
|
||||
*/
|
||||
public function isValidHashtag($hashtag = null)
|
||||
{
|
||||
if (is_null($hashtag)) {
|
||||
$hashtag = $this->tweet;
|
||||
}
|
||||
$length = StringUtils::strlen($hashtag);
|
||||
if (empty($hashtag) || !$length) {
|
||||
return false;
|
||||
}
|
||||
$extracted = $this->extractor->extractHashtags($hashtag);
|
||||
return count($extracted) === 1 && $extracted[0] === substr($hashtag, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a hashtag is valid.
|
||||
*
|
||||
* @return boolean Whether the hashtag is valid.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function validateHashtag()
|
||||
{
|
||||
return $this->isValidHashtag();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a URL is valid.
|
||||
*
|
||||
* @param string $url The url to validate.
|
||||
* @param boolean $unicode_domains Consider the domain to be unicode.
|
||||
* @param boolean $require_protocol Require a protocol for valid domain?
|
||||
*
|
||||
* @return boolean Whether the URL is valid.
|
||||
*/
|
||||
public function isValidURL($url = null, $unicode_domains = true, $require_protocol = true)
|
||||
{
|
||||
if (is_null($url)) {
|
||||
$url = $this->tweet;
|
||||
}
|
||||
$length = StringUtils::strlen($url);
|
||||
if (empty($url) || !$length) {
|
||||
return false;
|
||||
}
|
||||
preg_match(self::$patterns['validate_url_unencoded'], $url, $matches);
|
||||
$match = array_shift($matches);
|
||||
if (!$matches || $match !== $url) {
|
||||
return false;
|
||||
}
|
||||
list($scheme, $authority, $path, $query, $fragment) = array_pad($matches, 5, '');
|
||||
# Check scheme, path, query, fragment:
|
||||
if (($require_protocol && !(
|
||||
self::isValidMatch($scheme, self::$patterns['validate_url_scheme']) && preg_match('/^https?$/i', $scheme))
|
||||
) || !self::isValidMatch($path, self::$patterns['validate_url_path']) || !self::isValidMatch($query, self::$patterns['validate_url_query'], true)
|
||||
|| !self::isValidMatch($fragment, self::$patterns['validate_url_fragment'], true)) {
|
||||
return false;
|
||||
}
|
||||
# Check authority:
|
||||
$authority_pattern = $unicode_domains ? 'validate_url_unicode_authority' : 'validate_url_authority';
|
||||
return self::isValidMatch($authority, self::$patterns[$authority_pattern]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a URL is valid.
|
||||
*
|
||||
* @param boolean $unicode_domains Consider the domain to be unicode.
|
||||
* @param boolean $require_protocol Require a protocol for valid domain?
|
||||
*
|
||||
* @return boolean Whether the URL is valid.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function validateURL($unicode_domains = true, $require_protocol = true)
|
||||
{
|
||||
return $this->isValidURL(null, $unicode_domains, $require_protocol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the length of a tweet. Takes shortening of URLs into account.
|
||||
*
|
||||
* @param string $tweet The tweet to validate.
|
||||
* @return int the length of a tweet.
|
||||
*/
|
||||
public function getTweetLength($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$length = StringUtils::strlen($tweet);
|
||||
$urls_with_indices = $this->extractor->extractURLsWithIndices($tweet);
|
||||
foreach ($urls_with_indices as $x) {
|
||||
$length += $x['indices'][0] - $x['indices'][1];
|
||||
$length += stripos($x['url'], 'https://') === 0 ? $this->short_url_length_https : $this->short_url_length;
|
||||
}
|
||||
return $length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the length of a tweet. Takes shortening of URLs into account.
|
||||
*
|
||||
* @return int the length of a tweet.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function getLength()
|
||||
{
|
||||
return $this->getTweetLength();
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to check for a valid match. Used in URL validation.
|
||||
*
|
||||
* @param string $string The subject string to test.
|
||||
* @param string $pattern The pattern to match against.
|
||||
* @param boolean $optional Whether a match is compulsory or not.
|
||||
*
|
||||
* @return boolean Whether an exact match was found.
|
||||
*/
|
||||
protected static function isValidMatch($string, $pattern, $optional = false)
|
||||
{
|
||||
$found = preg_match($pattern, $string, $matches);
|
||||
if (!$optional) {
|
||||
return (($string || $string === '') && $found && $matches[0] === $string);
|
||||
} else {
|
||||
return !(($string || $string === '') && (!$found || $matches[0] !== $string));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateMentionsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('mentions', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('status_id')->unsigned();
|
||||
$table->bigInteger('profile_id')->unsigned();
|
||||
$table->boolean('local')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('mentions');
|
||||
}
|
||||
}
|
BIN
public/css/app.css
vendored
BIN
public/css/app.css
vendored
Binary file not shown.
BIN
public/js/activity.js
vendored
Normal file
BIN
public/js/activity.js
vendored
Normal file
Binary file not shown.
BIN
public/js/timeline.js
vendored
BIN
public/js/timeline.js
vendored
Binary file not shown.
Binary file not shown.
10
resources/assets/js/activity.js
vendored
Normal file
10
resources/assets/js/activity.js
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
$(document).ready(function() {
|
||||
$('.pagination').hide();
|
||||
let elem = document.querySelector('.notification-page .list-group');
|
||||
let infScroll = new InfiniteScroll( elem, {
|
||||
path: '.pagination__next',
|
||||
append: '.notification-page .list-group',
|
||||
status: '.page-load-status',
|
||||
history: true,
|
||||
});
|
||||
});
|
14
resources/assets/sass/_variables.scss
vendored
14
resources/assets/sass/_variables.scss
vendored
|
@ -6,3 +6,17 @@ $body-bg: #f5f8fa;
|
|||
$font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
$font-size-base: 0.9rem;
|
||||
$line-height-base: 1.6;
|
||||
|
||||
$font-size-lg: ($font-size-base * 1.25);
|
||||
$font-size-sm: ($font-size-base * .875);
|
||||
$input-height: 2.375rem;
|
||||
$input-height-sm: 1.9375rem;
|
||||
$input-height-lg: 3rem;
|
||||
$input-btn-focus-width: .2rem;
|
||||
$custom-control-indicator-bg: #dee2e6;
|
||||
$custom-control-indicator-disabled-bg: #e9ecef;
|
||||
$custom-control-description-disabled-color: #868e96;
|
||||
$white: white;
|
||||
$theme-colors: (
|
||||
'primary': #08d
|
||||
);
|
2
resources/assets/sass/app.scss
vendored
2
resources/assets/sass/app.scss
vendored
|
@ -14,3 +14,5 @@
|
|||
@import "components/typeahead";
|
||||
|
||||
@import "components/notifications";
|
||||
|
||||
@import "components/switch";
|
152
resources/assets/sass/components/switch.scss
vendored
Normal file
152
resources/assets/sass/components/switch.scss
vendored
Normal file
|
@ -0,0 +1,152 @@
|
|||
$switch-height: calc(#{$input-height} * .8) !default;
|
||||
$switch-height-sm: calc(#{$input-height-sm} * .8) !default;
|
||||
$switch-height-lg: calc(#{$input-height-lg} * .8) !default;
|
||||
$switch-border-radius: $switch-height !default;
|
||||
$switch-bg: $custom-control-indicator-bg !default;
|
||||
$switch-checked-bg: map-get($theme-colors, 'danger') !default;
|
||||
$switch-disabled-bg: $custom-control-indicator-disabled-bg !default;
|
||||
$switch-disabled-color: $custom-control-description-disabled-color !default;
|
||||
$switch-thumb-bg: $white !default;
|
||||
$switch-thumb-border-radius: 50% !default;
|
||||
$switch-thumb-padding: 2px !default;
|
||||
$switch-focus-box-shadow: 0 0 0 $input-btn-focus-width rgba(map-get($theme-colors, 'primary'), .25);
|
||||
$switch-transition: .2s all !default;
|
||||
|
||||
.switch {
|
||||
font-size: $font-size-base;
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
background: none;
|
||||
border: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
|
||||
+ label {
|
||||
position: relative;
|
||||
min-width: calc(#{$switch-height} * 2);
|
||||
border-radius: $switch-border-radius;
|
||||
height: $switch-height;
|
||||
line-height: $switch-height;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
text-indent: calc(calc(#{$switch-height} * 2) + .5rem);
|
||||
}
|
||||
|
||||
+ label::before,
|
||||
+ label::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: calc(#{$switch-height} * 2);
|
||||
bottom: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
+ label::before {
|
||||
right: 0;
|
||||
background-color: $switch-bg;
|
||||
border-radius: $switch-border-radius;
|
||||
transition: $switch-transition;
|
||||
}
|
||||
|
||||
+ label::after {
|
||||
top: $switch-thumb-padding;
|
||||
left: $switch-thumb-padding;
|
||||
width: calc(#{$switch-height} - calc(#{$switch-thumb-padding} * 2));
|
||||
height: calc(#{$switch-height} - calc(#{$switch-thumb-padding} * 2));
|
||||
border-radius: $switch-thumb-border-radius;
|
||||
background-color: $switch-thumb-bg;
|
||||
transition: $switch-transition;
|
||||
}
|
||||
|
||||
&:checked + label::before {
|
||||
background-color: $switch-checked-bg;
|
||||
}
|
||||
|
||||
&:checked + label::after {
|
||||
margin-left: $switch-height;
|
||||
}
|
||||
|
||||
&:focus + label::before {
|
||||
outline: none;
|
||||
box-shadow: $switch-focus-box-shadow;
|
||||
}
|
||||
|
||||
&:disabled + label {
|
||||
color: $switch-disabled-color;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:disabled + label::before {
|
||||
background-color: $switch-disabled-bg;
|
||||
}
|
||||
}
|
||||
|
||||
// Small variation
|
||||
&.switch-sm {
|
||||
font-size: $font-size-sm;
|
||||
|
||||
input {
|
||||
+ label {
|
||||
min-width: calc(#{$switch-height-sm} * 2);
|
||||
height: $switch-height-sm;
|
||||
line-height: $switch-height-sm;
|
||||
text-indent: calc(calc(#{$switch-height-sm} * 2) + .5rem);
|
||||
}
|
||||
|
||||
+ label::before {
|
||||
width: calc(#{$switch-height-sm} * 2);
|
||||
}
|
||||
|
||||
+ label::after {
|
||||
width: calc(#{$switch-height-sm} - calc(#{$switch-thumb-padding} * 2));
|
||||
height: calc(#{$switch-height-sm} - calc(#{$switch-thumb-padding} * 2));
|
||||
}
|
||||
|
||||
&:checked + label::after {
|
||||
margin-left: $switch-height-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Large variation
|
||||
&.switch-lg {
|
||||
font-size: $font-size-lg;
|
||||
|
||||
input {
|
||||
+ label {
|
||||
min-width: calc(#{$switch-height-lg} * 2);
|
||||
height: $switch-height-lg;
|
||||
line-height: $switch-height-lg;
|
||||
text-indent: calc(calc(#{$switch-height-lg} * 2) + .5rem);
|
||||
}
|
||||
|
||||
+ label::before {
|
||||
width: calc(#{$switch-height-lg} * 2);
|
||||
}
|
||||
|
||||
+ label::after {
|
||||
width: calc(#{$switch-height-lg} - calc(#{$switch-thumb-padding} * 2));
|
||||
height: calc(#{$switch-height-lg} - calc(#{$switch-thumb-padding} * 2));
|
||||
}
|
||||
|
||||
&:checked + label::after {
|
||||
margin-left: $switch-height-lg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+ .switch {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
31
resources/assets/sass/custom.scss
vendored
31
resources/assets/sass/custom.scss
vendored
|
@ -10,6 +10,7 @@ body {
|
|||
#content {
|
||||
margin-bottom: auto !important;
|
||||
}
|
||||
|
||||
body, button, input, textarea {
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",
|
||||
Roboto,Helvetica,Arial,sans-serif;
|
||||
|
@ -203,3 +204,33 @@ body, button, input, textarea {
|
|||
height: .25rem;
|
||||
animation: loading-bar 3s linear infinite;
|
||||
}
|
||||
|
||||
.max-hide-overflow {
|
||||
max-height: 500px;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
@media (min-width: map-get($grid-breakpoints, "xs")) {
|
||||
.max-hide-overflow {
|
||||
max-height: 600px!important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: map-get($grid-breakpoints, "md")) {
|
||||
.max-hide-overflow {
|
||||
max-height: 800px!important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: map-get($grid-breakpoints, "xl")) {
|
||||
.max-hide-overflow {
|
||||
max-height: 1000px!important;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-image {
|
||||
background-size: cover;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-position: 50%;
|
||||
}
|
||||
|
|
13
resources/lang/en/navmenu.php
Normal file
13
resources/lang/en/navmenu.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'viewMyProfile' => 'View my profile',
|
||||
'myTimeline' => 'My Timeline',
|
||||
'publicTimeline' => 'Public Timeline',
|
||||
'remoteFollow' => 'Remote Follow',
|
||||
'settings' => 'Settings',
|
||||
'admin' => 'Admin',
|
||||
'logout' => 'Logout',
|
||||
|
||||
];
|
|
@ -5,5 +5,6 @@ return [
|
|||
'likedPhoto' => 'liked your photo.',
|
||||
'startedFollowingYou' => 'started following you.',
|
||||
'commented' => 'commented on your post.',
|
||||
'mentionedYou' => 'mentioned you.'
|
||||
|
||||
];
|
|
@ -19,7 +19,7 @@
|
|||
<span class="text-muted notification-timestamp pl-1">{{$notification->created_at->diffForHumans(null, true, true, true)}}</span>
|
||||
</span>
|
||||
<span class="float-right notification-action">
|
||||
@if($notification->item_id)
|
||||
@if($notification->item_id && $notification->item_type == 'App\Status')
|
||||
<a href="{{$notification->status->url()}}"><img src="{{$notification->status->thumb()}}" width="32px" height="32px"></a>
|
||||
@endif
|
||||
</span>
|
||||
|
@ -54,7 +54,32 @@
|
|||
</span>
|
||||
<span class="float-right notification-action">
|
||||
@if($notification->item_id)
|
||||
<a href="{{$notification->status->parent()->url()}}"><img src="{{$notification->status->parent()->thumb()}}" width="32px" height="32px"></a>
|
||||
<a href="{{$notification->status->parent()->url()}}">
|
||||
<div class="notification-image" style="background-image: url('{{$notification->status->parent()->thumb()}}')"></div>
|
||||
</a>
|
||||
@endif
|
||||
</span>
|
||||
@break
|
||||
|
||||
@case('mention')
|
||||
<span class="notification-icon pr-3">
|
||||
<img src="{{$notification->status->profile->avatarUrl()}}" width="32px" class="rounded-circle">
|
||||
</span>
|
||||
<span class="notification-text">
|
||||
{!! $notification->rendered !!}
|
||||
<span class="text-muted notification-timestamp pl-1">{{$notification->created_at->diffForHumans(null, true, true, true)}}</span>
|
||||
</span>
|
||||
<span class="float-right notification-action">
|
||||
@if($notification->item_id && $notification->item_type === 'App\Status')
|
||||
@if(is_null($notification->status->in_reply_to_id))
|
||||
<a href="{{$notification->status->url()}}">
|
||||
<div class="notification-image" style="background-image: url('{{$notification->status->thumb()}}')"></div>
|
||||
</a>
|
||||
@else
|
||||
<a href="{{$notification->status->parent()->url()}}">
|
||||
<div class="notification-image" style="background-image: url('{{$notification->status->parent()->thumb()}}')"></div>
|
||||
</a>
|
||||
@endif
|
||||
@endif
|
||||
</span>
|
||||
@break
|
||||
|
@ -62,12 +87,20 @@
|
|||
@endswitch
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
|
||||
<div class="d-flex justify-content-center my-4">
|
||||
{{$notifications->links()}}
|
||||
</div>
|
||||
@else
|
||||
<div class="mt-4">
|
||||
<div class="alert alert-info font-weight-bold">No unread notifications found.</div>
|
||||
</div>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript" src="{{mix('js/activity.js')}}"></script>
|
||||
@endpush
|
||||
|
|
|
@ -20,11 +20,9 @@
|
|||
|
||||
<meta name="medium" content="image">
|
||||
<meta name="theme-color" content="#10c5f8">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
|
||||
<link rel="canonical" href="{{request()->url()}}">
|
||||
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
|
||||
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/simple-line-icons/2.4.1/css/simple-line-icons.min.css" integrity="sha256-7O1DfUu4pybYI7uAATw34eDrgQaWGOfMV/8erfDQz/Q=" crossorigin="anonymous" />
|
||||
<link href="{{ mix('css/app.css') }}" rel="stylesheet">
|
||||
@stack('styles')
|
||||
</head>
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
<footer>
|
||||
<div class="container py-3">
|
||||
<div class="container py-5">
|
||||
<p class="mb-0 text-uppercase font-weight-bold small">
|
||||
<a href="{{route('site.about')}}" class="text-primary pr-2">About Us</a>
|
||||
<a href="{{route('site.help')}}" class="text-primary pr-2">Support</a>
|
||||
<a href="" class="text-primary pr-2">API</a>
|
||||
<a href="{{route('site.opensource')}}" class="text-primary pr-2">Open Source</a>
|
||||
<a href="{{route('site.language')}}" class="text-primary pr-2">Language</a>
|
||||
<span class="px-2"></span>
|
||||
<a href="{{route('site.terms')}}" class="text-primary pr-2 pl-2">Terms</a>
|
||||
<a href="{{route('site.privacy')}}" class="text-primary pr-2">Privacy</a>
|
||||
<a href="{{route('site.terms')}}" class="text-primary pr-2">Terms</a>
|
||||
<a href="#" class="text-primary pr-2">Directory</a>
|
||||
<a href="{{route('site.platform')}}" class="text-primary pr-2">API</a>
|
||||
<span class="px-2"></span>
|
||||
<a href="#" class="text-primary pr-2 pl-2">Directory</a>
|
||||
<a href="#" class="text-primary pr-2">Profiles</a>
|
||||
<a href="#" class="text-primary pr-2">Hashtags</a>
|
||||
<a href="{{route('site.language')}}" class="text-primary">Language</a>
|
||||
<a href="#" class="text-dark float-right">© {{date('Y')}} PixelFed.org</a>
|
||||
<a href="#" class="text-primary">Hashtags</a>
|
||||
<a href="http://pixelfed.org" class="text-muted float-right" rel="noopener">Powered by PixelFed</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<nav class="navbar navbar-expand navbar-light navbar-laravel sticky-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-flex align-items-center" href="{{ url('/timeline') }}">
|
||||
<a class="navbar-brand d-flex align-items-center" href="{{ url('/timeline') }}" title="Logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>
|
||||
<strong class="font-weight-bold">{{ config('app.name', 'Laravel') }}</strong>
|
||||
</a>
|
||||
|
@ -18,37 +18,52 @@
|
|||
<li><a class="nav-link font-weight-bold" href="{{ route('register') }}">{{ __('Register') }}</a></li>
|
||||
@else
|
||||
<li class="nav-item px-2">
|
||||
<a class="nav-link" href="{{route('discover')}}"><i class="lead icon-compass"></i></a>
|
||||
<a class="nav-link" href="{{route('discover')}}" title="Discover"><i class="far fa-compass fa-lg"></i></a>
|
||||
</li>
|
||||
<li class="nav-item px-2">
|
||||
<a class="nav-link" href="{{route('notifications')}}"><i class="lead icon-heart"></i></a>
|
||||
<a class="nav-link" href="{{route('notifications')}}" title="Notifications"><i class="far fa-heart fa-lg"></i></a>
|
||||
</li>
|
||||
<li class="nav-item dropdown px-2">
|
||||
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
|
||||
<i class="lead icon-user"></i> <span class="caret"></span>
|
||||
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre title="User Menu">
|
||||
<i class="far fa-user fa-lg"></i> <span class="caret"></span>
|
||||
</a>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
|
||||
@if(Auth::user()->is_admin == true)
|
||||
<a class="dropdown-item font-weight-bold" href="{{ route('admin.home') }}">Admin</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
@endif
|
||||
<a class="dropdown-item font-weight-bold" href="{{ Auth::user()->url() }}">My Profile</a>
|
||||
<a class="dropdown-item font-weight-bold" href="{{route('settings')}}">Settings</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item font-weight-bold" href="{{route('remotefollow')}}">Remote Follow</a>
|
||||
<a class="dropdown-item font-weight-ultralight text-truncate" href="{{Auth::user()->url()}}">
|
||||
<img class="img-thumbnail rounded-circle pr-1" src="{{Auth::user()->profile->avatarUrl()}}" width="32px">
|
||||
@{{Auth::user()->username}}
|
||||
<p class="small mb-0 text-muted">{{__('navmenu.viewMyProfile')}}</p>
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item font-weight-bold" href="{{route('timeline.personal')}}">
|
||||
My Timeline
|
||||
<span class="fas fa-list-alt pr-1"></span>
|
||||
{{__('navmenu.myTimeline')}}
|
||||
</a>
|
||||
<a class="dropdown-item font-weight-bold" href="{{route('timeline.public')}}">
|
||||
Public Timeline
|
||||
<span class="far fa-list-alt pr-1"></span>
|
||||
{{__('navmenu.publicTimeline')}}
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item font-weight-bold" href="{{route('remotefollow')}}">
|
||||
<span class="fas fa-user-plus pr-1"></span>
|
||||
{{__('navmenu.remoteFollow')}}
|
||||
</a>
|
||||
<a class="dropdown-item font-weight-bold" href="{{route('settings')}}">
|
||||
<span class="fas fa-cog pr-1"></span>
|
||||
{{__('navmenu.settings')}}
|
||||
</a>
|
||||
@if(Auth::user()->is_admin == true)
|
||||
<a class="dropdown-item font-weight-bold" href="{{ route('admin.home') }}">
|
||||
<span class="fas fa-cogs pr-1"></span>
|
||||
{{__('navmenu.admin')}}
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
@endif
|
||||
<a class="dropdown-item font-weight-bold" href="{{ route('logout') }}"
|
||||
onclick="event.preventDefault();
|
||||
document.getElementById('logout-form').submit();">
|
||||
{{ __('Logout') }}
|
||||
<span class="fas fa-sign-out-alt pr-1"></span>
|
||||
{{ __('navmenu.logout') }}
|
||||
</a>
|
||||
|
||||
<form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app',['title' => $user->username . "'s followers"])
|
||||
@extends('layouts.app',['title' => $profile->username . "'s followers"])
|
||||
|
||||
@section('content')
|
||||
|
||||
|
@ -60,6 +60,6 @@
|
|||
@endsection
|
||||
|
||||
@push('meta')
|
||||
<meta property="og:description" content="{{$user->bio}}">
|
||||
<meta property="og:image" content="{{$user->avatarUrl()}}">
|
||||
<meta property="og:description" content="{{$profile->bio}}">
|
||||
<meta property="og:image" content="{{$profile->avatarUrl()}}">
|
||||
@endpush
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app',['title' => $user->username . "'s follows"])
|
||||
@extends('layouts.app',['title' => $profile->username . "'s follows"])
|
||||
|
||||
@section('content')
|
||||
|
||||
|
@ -60,6 +60,6 @@
|
|||
@endsection
|
||||
|
||||
@push('meta')
|
||||
<meta property="og:description" content="{{$user->bio}}">
|
||||
<meta property="og:image" content="{{$user->avatarUrl()}}">
|
||||
<meta property="og:description" content="{{$profile->bio}}">
|
||||
<meta property="og:image" content="{{$profile->avatarUrl()}}">
|
||||
@endpush
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="bg-white py-5">
|
||||
<div class="bg-white py-5 border-bottom">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-4 d-flex">
|
||||
|
@ -8,11 +8,16 @@
|
|||
</div>
|
||||
<div class="col-12 col-md-8 d-flex align-items-center">
|
||||
<div class="profile-details">
|
||||
<div class="username-bar pb-2 d-flex align-items-center">
|
||||
<div class="username-bar pb-2 d-flex align-items-center">
|
||||
<span class="font-weight-ultralight h1">{{$user->username}}</span>
|
||||
@if($is_admin == true)
|
||||
<span class="pl-4">
|
||||
<span class="btn btn-outline-danger font-weight-bold py-0">ADMIN</span>
|
||||
</span>
|
||||
@endif
|
||||
@if($owner == true)
|
||||
<span class="h5 pl-2 b-0">
|
||||
<a class="icon-settings text-muted" href="{{route('settings')}}"></a>
|
||||
<span class="pl-4">
|
||||
<a class="fas fa-cog fa-lg text-muted" href="{{route('settings')}}"></a>
|
||||
</span>
|
||||
@elseif ($is_following == true)
|
||||
<span class="pl-4">
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
@if($owner == true)
|
||||
<div>
|
||||
<ul class="nav nav-topbar d-flex justify-content-center">
|
||||
<ul class="nav nav-topbar d-flex justify-content-center border-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{request()->is('*/saved') ? '':'active'}} font-weight-bold text-uppercase" href="{{$user->url()}}">Posts</a>
|
||||
</li>
|
||||
|
@ -32,10 +32,10 @@
|
|||
<div class="info-overlay-text">
|
||||
<h5 class="text-white m-auto font-weight-bold">
|
||||
<span class="pr-4">
|
||||
<span class="icon-heart pr-1"></span> {{$status->likes_count}}
|
||||
<span class="far fa-heart fa-lg pr-1"></span> {{$status->likes_count}}
|
||||
</span>
|
||||
<span>
|
||||
<span class="icon-speech pr-1"></span> {{$status->comments_count}}
|
||||
<span class="far fa-comment fa-lg pr-1"></span> {{$status->comments_count}}
|
||||
</span>
|
||||
</h5>
|
||||
</div>
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
<p class="lead">Fediverse is a portmanteau of "federation" and "universe". It is a common, informal name for a somewhat broad federation of social network servers.</p>
|
||||
<p class="lead font-weight-bold text-muted mt-4">Supported Fediverse Projects</p>
|
||||
<ul class="lead pl-4">
|
||||
<li>Mastodon - A federated twitter alternative.</li>
|
||||
<li>Pleroma - A federated twitter alternative.</li>
|
||||
<li><a href="https://joinmastodon.org" rel="nofollow noopener">Mastodon</a> - A federated twitter alternative.</li>
|
||||
<li><a href="https://pleroma.social" rel="nofollow noopener">Pleroma</a> - A federated twitter alternative.</li>
|
||||
</ul>
|
||||
</section>
|
||||
@endsection
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="container px-0 mt-md-4">
|
||||
<div class="card status-container orientation-{{$status->firstMedia()->orientation ?? 'unknown'}}">
|
||||
<div class="row mx-0">
|
||||
<div class="d-flex d-md-none align-items-center justify-content-between card-header w-100">
|
||||
<div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100">
|
||||
<div class="d-flex align-items-center status-username">
|
||||
<div class="status-avatar mr-2">
|
||||
<img class="img-thumbnail" src="{{$user->avatarUrl()}}" width="24px" height="24px" style="border-radius:12px;">
|
||||
|
@ -14,15 +14,12 @@
|
|||
<a href="{{$user->url()}}" class="username-link font-weight-bold text-dark">{{$user->username}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timestamp mb-0">
|
||||
<p class="small text-uppercase mb-0"><a href="{{$status->url()}}" class="text-muted">{{$status->created_at->diffForHumans(null, true, true, true)}}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-8 status-photo px-0">
|
||||
<img src="{{$status->mediaUrl()}}" width="100%">
|
||||
</div>
|
||||
<div class="col-12 col-md-4 px-0 d-flex flex-column border-left border-md-left-0">
|
||||
<div class="d-md-flex d-none align-items-center justify-content-between card-header">
|
||||
<div class="d-md-flex d-none align-items-center justify-content-between card-header py-3 bg-white">
|
||||
<div class="d-flex align-items-center status-username">
|
||||
<div class="status-avatar mr-2">
|
||||
<img class="img-thumbnail" src="{{$user->avatarUrl()}}" width="24px" height="24px" style="border-radius:12px;">
|
||||
|
@ -31,9 +28,6 @@
|
|||
<a href="{{$user->url()}}" class="username-link font-weight-bold text-dark">{{$user->username}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timestamp mb-0">
|
||||
<p class="small text-uppercase mb-0"><a href="{{$status->url()}}" class="text-muted">{{$status->created_at->diffForHumans(null, true, true, true)}}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-md-column flex-column-reverse h-100">
|
||||
<div class="card-body status-comments">
|
||||
|
@ -52,23 +46,23 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body flex-grow-0">
|
||||
<div class="card-body flex-grow-0 py-1">
|
||||
<div class="reactions h3 mb-0">
|
||||
<form class="d-inline-flex like-form pr-3" method="post" action="/i/like" style="display: inline;" data-id="{{$status->id}}" data-action="like">
|
||||
@csrf
|
||||
<input type="hidden" name="item" value="{{$status->id}}">
|
||||
<button class="btn btn-link text-dark btn-lg p-0" type="submit">
|
||||
<button class="btn btn-link text-dark btn-lg p-0" type="submit" title="Like!">
|
||||
<span class="far fa-heart fa-lg mb-0"></span>
|
||||
</button>
|
||||
</form>
|
||||
<span class="far fa-comment pt-1 pr-3"></span>
|
||||
<span class="far fa-comment pt-1 pr-3" title="Comment"></span>
|
||||
@if(Auth::check())
|
||||
@if(Auth::user()->profile->id === $status->profile->id || Auth::user()->is_admin == true)
|
||||
<form method="post" action="/i/delete" class="d-inline-flex">
|
||||
@csrf
|
||||
<input type="hidden" name="type" value="post">
|
||||
<input type="hidden" name="item" value="{{$status->id}}">
|
||||
<button type="submit" class="btn btn-link btn-lg text-dark p-0">
|
||||
<button type="submit" class="btn btn-link btn-lg text-dark p-0" title="Remove">
|
||||
<span class="far fa-trash-alt fa-lg mb-0"></span>
|
||||
</button>
|
||||
</form>
|
||||
|
@ -78,7 +72,7 @@
|
|||
<form class="d-inline-flex bookmark-form" method="post" action="/i/bookmark" style="display: inline;" data-id="{{$status->id}}" data-action="bookmark">
|
||||
@csrf
|
||||
<input type="hidden" name="item" value="{{$status->id}}">
|
||||
<button class="btn btn-link text-dark p-0 btn-lg" type="submit">
|
||||
<button class="btn btn-link text-dark p-0 btn-lg" type="submit" title="Save">
|
||||
<span class="far fa-bookmark fa-lg mb-0"></span>
|
||||
</button>
|
||||
</form>
|
||||
|
@ -87,9 +81,14 @@
|
|||
<div class="likes font-weight-bold mb-0">
|
||||
<span class="like-count" data-count="{{$status->likes_count}}">{{$status->likes_count}}</span> likes
|
||||
</div>
|
||||
<div class="timestamp">
|
||||
<a href="{{$status->url()}}" class="small text-muted">
|
||||
{{$status->created_at->format('F j, Y')}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-light sticky-md-bottom">
|
||||
<div class="card-footer bg-white sticky-md-bottom">
|
||||
<form class="comment-form" method="post" action="/i/comment" data-id="{{$status->id}}" data-truncate="false">
|
||||
@csrf
|
||||
<input type="hidden" name="item" value="{{$status->id}}">
|
||||
|
@ -104,6 +103,6 @@
|
|||
@endsection
|
||||
|
||||
@push('meta')
|
||||
<meta property="og:description" content="{!! $status->rendered ?? e($status->caption) !!}">
|
||||
<meta property="og:description" content="{{ $status->caption }}">
|
||||
<meta property="og:image" content="{{$status->mediaUrl()}}">
|
||||
@endpush
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<div class="card my-4 status-card">
|
||||
<div class="card-header d-inline-flex align-items-center bg-white">
|
||||
<img class="img-thumbnail" src="{{$item->profile->avatarUrl()}}" width="32px" height="32px" style="border-radius: 32px;">
|
||||
<img src="{{$item->profile->avatarUrl()}}" width="32px" height="32px" style="border-radius: 32px;">
|
||||
<a class="username font-weight-bold pl-2 text-dark" href="{{$item->profile->url()}}">
|
||||
{{$item->profile->username}}
|
||||
</a>
|
||||
<div class="text-right" style="flex-grow:1;">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="icon-options"></span>
|
||||
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
|
||||
<span class="fas fa-ellipsis-v fa-lg text-muted"></span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item" href="{{$item->url()}}">Go to post</a>
|
||||
|
@ -28,24 +28,35 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{$item->url()}}">
|
||||
@if($item->is_nsfw)
|
||||
<details>
|
||||
<p>
|
||||
<summary>NSFW / Hidden Image</summary>
|
||||
<a class="max-hide-overflow" href="{{$item->url()}}">
|
||||
<img class="card-img-top" src="{{$item->mediaUrl()}}">
|
||||
</a>
|
||||
</p>
|
||||
</details>
|
||||
@else
|
||||
<a class="max-hide-overflow" href="{{$item->url()}}">
|
||||
<img class="card-img-top" src="{{$item->mediaUrl()}}">
|
||||
</a>
|
||||
@endif
|
||||
<div class="card-body">
|
||||
<div class="reactions h3">
|
||||
<form class="like-form pr-3" method="post" action="/i/like" style="display: inline;" data-id="{{$item->id}}" data-action="like" data-count="{{$item->likes_count}}">
|
||||
@csrf
|
||||
<input type="hidden" name="item" value="{{$item->id}}">
|
||||
<button class="btn btn-link text-dark p-0" type="submit">
|
||||
<button class="btn btn-link text-dark p-0" type="submit" title=""Like!>
|
||||
<span class="far fa-heart status-heart fa-2x"></span>
|
||||
</button>
|
||||
</form>
|
||||
<span class="far fa-comment status-comment-focus"></span>
|
||||
<span class="far fa-comment status-comment-focus" title="Comment"></span>
|
||||
<span class="float-right">
|
||||
<form class="bookmark-form" method="post" action="/i/bookmark" style="display: inline;" data-id="{{$item->id}}" data-action="bookmark">
|
||||
@csrf
|
||||
<input type="hidden" name="item" value="{{$item->id}}">
|
||||
<button class="btn btn-link text-dark p-0" type="submit"><span class="far fa-bookmark" style="font-size:25px;"></span></button>
|
||||
<button class="btn btn-link text-dark p-0" type="submit" title="Save"><span class="far fa-bookmark" style="font-size:25px;"></span></button>
|
||||
</form>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -12,11 +12,21 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<label class="font-weight-bold text-muted small">Caption</label>
|
||||
<input type="text" class="form-control" name="caption" placeholder="Add a caption here">
|
||||
<input type="text" class="form-control" name="caption" placeholder="Add a caption here" autocomplete="off">
|
||||
<small class="form-text text-muted">
|
||||
Max length: {{config('pixelfed.max_caption_length')}} characters.
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="font-weight-bold text-muted small">CW/NSFW</label>
|
||||
<div class="switch switch-sm">
|
||||
<input type="checkbox" class="switch" id="cw-switch" name="cw">
|
||||
<label for="cw-switch" class="small font-weight-bold">(Default off)</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">
|
||||
Please mark all NSFW and controversial content, as per our content policy.
|
||||
</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline-primary btn-block">Post</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
1
webpack.mix.js
vendored
1
webpack.mix.js
vendored
|
@ -12,6 +12,7 @@ let mix = require('laravel-mix');
|
|||
*/
|
||||
|
||||
mix.js('resources/assets/js/app.js', 'public/js')
|
||||
.js('resources/assets/js/activity.js', 'public/js')
|
||||
.js('resources/assets/js/timeline.js', 'public/js')
|
||||
.sass('resources/assets/sass/app.scss', 'public/css')
|
||||
.version();
|
||||
|
|
Loading…
Reference in a new issue