Merge pull request #2435 from pixelfed/staging

Add Direct Messages
This commit is contained in:
daniel 2020-11-18 23:51:21 -07:00 committed by GitHub
commit 5cfa08946e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 2215 additions and 141 deletions

View File

@ -2,6 +2,7 @@
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.10.9...dev)
### Added
- Direct Messages ([d63569c](https://github.com/pixelfed/pixelfed/commit/d63569c))
- ActivityPubFetchService for signed GET requests ([8763bfc5](https://github.com/pixelfed/pixelfed/commit/8763bfc5))
- Custom content warnings for remote posts ([6afc61a4](https://github.com/pixelfed/pixelfed/commit/6afc61a4))
- Thai translations ([74cd536](https://github.com/pixelfed/pixelfed/commit/74cd536))
@ -15,7 +16,7 @@
- Add MediaTagService ([524c6d45](https://github.com/pixelfed/pixelfed/commit/524c6d45))
- Add MediaBlocklist feature ([ba1f7e7e](https://github.com/pixelfed/pixelfed/commit/ba1f7e7e))
- New Discover Layout, add trending hashtags, places and posts ([c251d41b](https://github.com/pixelfed/pixelfed/commit/c251d41b))
- Add password change email notification. ([de1cca4f](https://github.com/pixelfed/pixelfed/commit/de1cca4f))
- Add Password change email notification ([de1cca4f](https://github.com/pixelfed/pixelfed/commit/de1cca4f))
### Updated
- Updated PostComponent, fix remote urls ([42716ccc](https://github.com/pixelfed/pixelfed/commit/42716ccc))

View File

@ -14,7 +14,7 @@ class DirectMessage extends Model
public function url()
{
return url('/i/message/' . $this->to_id . '/' . $this->id);
return config('app.url') . '/account/direct/m/' . $this->status_id;
}
public function author()
@ -22,8 +22,29 @@ class DirectMessage extends Model
return $this->hasOne(Profile::class, 'id', 'from_id');
}
public function recipient()
{
return $this->hasOne(Profile::class, 'id', 'to_id');
}
public function me()
{
return Auth::user()->profile->id === $this->from_id;
}
public function toText()
{
$actorName = $this->author->username;
return "{$actorName} sent a direct message.";
}
public function toHtml()
{
$actorName = $this->author->username;
$actorUrl = $this->author->url();
$url = $this->url();
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> sent a <a href='{$url}' class='dm-link'>direct message</a>.";
}
}

View File

@ -13,6 +13,7 @@ use Illuminate\Http\Request;
use PragmaRX\Google2FA\Google2FA;
use App\Jobs\FollowPipeline\FollowPipeline;
use App\{
DirectMessage,
EmailVerification,
Follower,
FollowRequest,
@ -114,19 +115,17 @@ class AccountController extends Controller
}
}
public function messages()
{
return view('account.messages');
}
public function direct()
{
return view('account.direct');
}
public function showMessage(Request $request, $id)
public function directMessage(Request $request, $id)
{
return view('account.message');
$profile = Profile::where('id', '!=', $request->user()->profile_id)
// ->whereNull('domain')
->findOrFail($id);
return view('account.directmessage', compact('id'));
}
public function mute(Request $request)

View File

@ -2,57 +2,664 @@
namespace App\Http\Controllers;
use Auth;
use Auth, Cache;
use Illuminate\Http\Request;
use App\{
DirectMessage,
Media,
Notification,
Profile,
Status
Status,
User,
UserFilter,
UserSetting
};
use App\Services\MediaPathService;
use App\Services\MediaBlocklistService;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use Illuminate\Support\Str;
use App\Util\ActivityPub\Helpers;
use App\Services\WebfingerService;
class DirectMessageController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function __construct()
{
$this->middleware('auth');
}
public function inbox(Request $request)
{
$profile = Auth::user()->profile;
$inbox = DirectMessage::selectRaw('*, max(created_at) as createdAt')
->whereToId($profile->id)
->with(['author','status'])
->orderBy('createdAt', 'desc')
->groupBy('from_id')
->paginate(12);
return view('account.messages', compact('inbox'));
public function browse(Request $request)
{
$this->validate($request, [
'a' => 'nullable|string|in:inbox,sent,filtered',
'page' => 'nullable|integer|min:1|max:99'
]);
}
$profile = $request->user()->profile_id;
$action = $request->input('a', 'inbox');
$page = $request->input('page');
public function show(Request $request, int $pid, $mid)
{
$profile = Auth::user()->profile;
if($action == 'inbox') {
$dms = DirectMessage::selectRaw('*, max(created_at) as createdAt')
->whereToId($profile)
->with(['author','status'])
->whereIsHidden(false)
->groupBy('from_id')
->latest()
->when($page, function($q, $page) {
if($page > 1) {
return $q->offset($page * 8 - 8);
}
})
->limit(8)
->get()
->map(function($r) use($profile) {
return $r->from_id !== $profile ? [
'id' => (string) $r->from_id,
'name' => $r->author->name,
'username' => $r->author->username,
'avatar' => $r->author->avatarUrl(),
'url' => $r->author->url(),
'isLocal' => (bool) !$r->author->domain,
'domain' => $r->author->domain,
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
'lastMessage' => $r->status->caption,
'messages' => []
] : [
'id' => (string) $r->to_id,
'name' => $r->recipient->name,
'username' => $r->recipient->username,
'avatar' => $r->recipient->avatarUrl(),
'url' => $r->recipient->url(),
'isLocal' => (bool) !$r->recipient->domain,
'domain' => $r->recipient->domain,
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
'lastMessage' => $r->status->caption,
'messages' => []
];
});
}
if($pid !== $profile->id) {
abort(403);
}
if($action == 'sent') {
$dms = DirectMessage::selectRaw('*, max(created_at) as createdAt')
->whereFromId($profile)
->with(['author','status'])
->groupBy('to_id')
->orderBy('createdAt', 'desc')
->when($page, function($q, $page) {
if($page > 1) {
return $q->offset($page * 8 - 8);
}
})
->limit(8)
->get()
->map(function($r) use($profile) {
return $r->from_id !== $profile ? [
'id' => (string) $r->from_id,
'name' => $r->author->name,
'username' => $r->author->username,
'avatar' => $r->author->avatarUrl(),
'url' => $r->author->url(),
'isLocal' => (bool) !$r->author->domain,
'domain' => $r->author->domain,
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
'lastMessage' => $r->status->caption,
'messages' => []
] : [
'id' => (string) $r->to_id,
'name' => $r->recipient->name,
'username' => $r->recipient->username,
'avatar' => $r->recipient->avatarUrl(),
'url' => $r->recipient->url(),
'isLocal' => (bool) !$r->recipient->domain,
'domain' => $r->recipient->domain,
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
'lastMessage' => $r->status->caption,
'messages' => []
];
});
}
$msg = DirectMessage::whereToId($profile->id)
->findOrFail($mid);
if($action == 'filtered') {
$dms = DirectMessage::selectRaw('*, max(created_at) as createdAt')
->whereToId($profile)
->with(['author','status'])
->whereIsHidden(true)
->groupBy('from_id')
->orderBy('createdAt', 'desc')
->when($page, function($q, $page) {
if($page > 1) {
return $q->offset($page * 8 - 8);
}
})
->limit(8)
->get()
->map(function($r) use($profile) {
return $r->from_id !== $profile ? [
'id' => (string) $r->from_id,
'name' => $r->author->name,
'username' => $r->author->username,
'avatar' => $r->author->avatarUrl(),
'url' => $r->author->url(),
'isLocal' => (bool) !$r->author->domain,
'domain' => $r->author->domain,
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
'lastMessage' => $r->status->caption,
'messages' => []
] : [
'id' => (string) $r->to_id,
'name' => $r->recipient->name,
'username' => $r->recipient->username,
'avatar' => $r->recipient->avatarUrl(),
'url' => $r->recipient->url(),
'isLocal' => (bool) !$r->recipient->domain,
'domain' => $r->recipient->domain,
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
'lastMessage' => $r->status->caption,
'messages' => []
];
});
}
$thread = DirectMessage::whereIn('to_id', [$profile->id, $msg->from_id])
->whereIn('from_id', [$profile->id,$msg->from_id])
->orderBy('created_at', 'desc')
->paginate(30);
return response()->json($dms);
}
$thread = $thread->reverse();
public function create(Request $request)
{
$this->validate($request, [
'to_id' => 'required',
'message' => 'required|string|min:1|max:500',
'type' => 'required|in:text,emoji'
]);
return view('account.message', compact('msg', 'profile', 'thread'));
}
$profile = $request->user()->profile;
$recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id'));
public function compose(Request $request)
{
$profile = Auth::user()->profile;
}
abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403);
$msg = $request->input('message');
if((!$recipient->domain && $recipient->user->settings->public_dm == false) || $recipient->is_private) {
if($recipient->follows($profile) == true) {
$hidden = false;
} else {
$hidden = true;
}
} else {
$hidden = false;
}
$status = new Status;
$status->profile_id = $profile->id;
$status->caption = $msg;
$status->rendered = $msg;
$status->visibility = 'direct';
$status->scope = 'direct';
$status->in_reply_to_profile_id = $recipient->id;
$status->save();
$dm = new DirectMessage;
$dm->to_id = $recipient->id;
$dm->from_id = $profile->id;
$dm->status_id = $status->id;
$dm->is_hidden = $hidden;
$dm->type = $request->input('type');
$dm->save();
if(filter_var($msg, FILTER_VALIDATE_URL)) {
if(Helpers::validateUrl($msg)) {
$dm->type = 'link';
$dm->meta = [
'domain' => parse_url($msg, PHP_URL_HOST),
'local' => parse_url($msg, PHP_URL_HOST) ==
parse_url(config('app.url'), PHP_URL_HOST)
];
$dm->save();
}
}
$nf = UserFilter::whereUserId($recipient->id)
->whereFilterableId($profile->id)
->whereFilterableType('App\Profile')
->whereFilterType('dm.mute')
->exists();
if($recipient->domain == null && $hidden == false && !$nf) {
$notification = new Notification();
$notification->profile_id = $recipient->id;
$notification->actor_id = $profile->id;
$notification->action = 'dm';
$notification->message = $dm->toText();
$notification->rendered = $dm->toHtml();
$notification->item_id = $dm->id;
$notification->item_type = "App\DirectMessage";
$notification->save();
}
if($recipient->domain) {
$this->remoteDeliver($dm);
}
$res = [
'id' => (string) $dm->id,
'isAuthor' => $profile->id == $dm->from_id,
'reportId' => (string) $dm->status_id,
'hidden' => (bool) $dm->is_hidden,
'type' => $dm->type,
'text' => $dm->status->caption,
'media' => null,
'timeAgo' => $dm->created_at->diffForHumans(null,null,true),
'seen' => $dm->read_at != null,
'meta' => $dm->meta
];
return response()->json($res);
}
public function thread(Request $request)
{
$this->validate($request, [
'pid' => 'required'
]);
$uid = $request->user()->profile_id;
$pid = $request->input('pid');
$max_id = $request->input('max_id');
$min_id = $request->input('min_id');
$r = Profile::findOrFail($pid);
// $r = Profile::whereNull('domain')->findOrFail($pid);
if($min_id) {
$res = DirectMessage::select('*')
->where('id', '>', $min_id)
->where(function($q) use($pid,$uid) {
return $q->where([['from_id',$pid],['to_id',$uid]
])->orWhere([['from_id',$uid],['to_id',$pid]]);
})
->latest()
->take(8)
->get();
} else if ($max_id) {
$res = DirectMessage::select('*')
->where('id', '<', $max_id)
->where(function($q) use($pid,$uid) {
return $q->where([['from_id',$pid],['to_id',$uid]
])->orWhere([['from_id',$uid],['to_id',$pid]]);
})
->latest()
->take(8)
->get();
} else {
$res = DirectMessage::where(function($q) use($pid,$uid) {
return $q->where([['from_id',$pid],['to_id',$uid]
])->orWhere([['from_id',$uid],['to_id',$pid]]);
})
->latest()
->take(8)
->get();
}
$res = $res->map(function($s) use ($uid){
return [
'id' => (string) $s->id,
'hidden' => (bool) $s->is_hidden,
'isAuthor' => $uid == $s->from_id,
'type' => $s->type,
'text' => $s->status->caption,
'media' => $s->status->firstMedia() ? $s->status->firstMedia()->url() : null,
'timeAgo' => $s->created_at->diffForHumans(null,null,true),
'seen' => $s->read_at != null,
'reportId' => (string) $s->status_id,
'meta' => json_decode($s->meta,true)
];
});
$w = [
'id' => (string) $r->id,
'name' => $r->name,
'username' => $r->username,
'avatar' => $r->avatarUrl(),
'url' => $r->url(),
'muted' => UserFilter::whereUserId($uid)
->whereFilterableId($r->id)
->whereFilterableType('App\Profile')
->whereFilterType('dm.mute')
->first() ? true : false,
'isLocal' => (bool) !$r->domain,
'domain' => $r->domain,
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
'lastMessage' => '',
'messages' => $res
];
return response()->json($w, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function delete(Request $request)
{
$this->validate($request, [
'id' => 'required'
]);
$sid = $request->input('id');
$pid = $request->user()->profile_id;
$dm = DirectMessage::whereFromId($pid)
->whereStatusId($sid)
->firstOrFail();
$status = Status::whereProfileId($pid)
->findOrFail($dm->status_id);
if($dm->recipient->domain) {
$dmc = $dm;
$this->remoteDelete($dmc);
}
$status->delete();
$dm->delete();
return [200];
}
public function get(Request $request, $id)
{
$pid = $request->user()->profile_id;
$dm = DirectMessage::whereStatusId($id)->firstOrFail();
abort_if($pid !== $dm->to_id && $pid !== $dm->from_id, 404);
return response()->json($dm, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function mediaUpload(Request $request)
{
$this->validate($request, [
'file' => function() {
return [
'required',
'mimes:' . config('pixelfed.media_types'),
'max:' . config('pixelfed.max_photo_size'),
];
},
'to_id' => 'required'
]);
$user = $request->user();
$profile = $user->profile;
$recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id'));
abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403);
if((!$recipient->domain && $recipient->user->settings->public_dm == false) || $recipient->is_private) {
if($recipient->follows($profile) == true) {
$hidden = false;
} else {
$hidden = true;
}
} else {
$hidden = false;
}
if(config('pixelfed.enforce_account_limit') == true) {
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
return Media::whereUserId($user->id)->sum('size') / 1000;
});
$limit = (int) config('pixelfed.max_account_size');
if ($size >= $limit) {
abort(403, 'Account size limit reached.');
}
}
$photo = $request->file('file');
$mimes = explode(',', config('pixelfed.media_types'));
if(in_array($photo->getMimeType(), $mimes) == false) {
abort(403, 'Invalid or unsupported mime type.');
}
$storagePath = MediaPathService::get($user, 2) . Str::random(8);
$path = $photo->store($storagePath);
$hash = \hash_file('sha256', $photo);
abort_if(MediaBlocklistService::exists($hash) == true, 451);
$status = new Status;
$status->profile_id = $profile->id;
$status->caption = null;
$status->rendered = null;
$status->visibility = 'direct';
$status->scope = 'direct';
$status->in_reply_to_profile_id = $recipient->id;
$status->save();
$media = new Media();
$media->status_id = $status->id;
$media->profile_id = $profile->id;
$media->user_id = $user->id;
$media->media_path = $path;
$media->original_sha256 = $hash;
$media->size = $photo->getSize();
$media->mime = $photo->getMimeType();
$media->caption = null;
$media->filter_class = null;
$media->filter_name = null;
$media->save();
$dm = new DirectMessage;
$dm->to_id = $recipient->id;
$dm->from_id = $profile->id;
$dm->status_id = $status->id;
$dm->type = array_first(explode('/', $media->mime)) == 'video' ? 'video' : 'photo';
$dm->is_hidden = $hidden;
$dm->save();
if($recipient->domain) {
$this->remoteDeliver($dm);
}
return [
'id' => $dm->id,
'reportId' => (string) $dm->status_id,
'type' => $dm->type,
'url' => $media->url()
];
}
public function composeLookup(Request $request)
{
$this->validate($request, [
'q' => 'required|string|min:2|max:50',
'remote' => 'nullable|boolean',
]);
$q = $request->input('q');
$r = $request->input('remote');
if($r && Helpers::validateUrl($q)) {
Helpers::profileFetch($q);
}
if(Str::of($q)->startsWith('@')) {
if(strlen($q) < 3) {
return [];
}
if(substr_count($q, '@') == 2) {
WebfingerService::lookup($q);
}
$q = mb_substr($q, 1);
}
$blocked = UserFilter::whereFilterableType('App\Profile')
->whereFilterType('block')
->whereFilterableId($request->user()->profile_id)
->pluck('user_id');
$blocked->push($request->user()->profile_id);
$results = Profile::select('id','domain','username')
->whereNotIn('id', $blocked)
->where('username','like','%'.$q.'%')
->orderBy('domain')
->limit(8)
->get()
->map(function($r) {
return [
'local' => (bool) !$r->domain,
'id' => (string) $r->id,
'name' => $r->username,
'privacy' => true,
'avatar' => $r->avatarUrl()
];
});
return $results;
}
public function read(Request $request)
{
$this->validate($request, [
'pid' => 'required',
'sid' => 'required'
]);
$pid = $request->input('pid');
$sid = $request->input('sid');
$dms = DirectMessage::whereToId($request->user()->profile_id)
->whereFromId($pid)
->where('status_id', '>=', $sid)
->get();
$now = now();
foreach($dms as $dm) {
$dm->read_at = $now;
$dm->save();
}
return response()->json($dms->pluck('id'));
}
public function mute(Request $request)
{
$this->validate($request, [
'id' => 'required'
]);
$fid = $request->input('id');
$pid = $request->user()->profile_id;
UserFilter::firstOrCreate(
[
'user_id' => $pid,
'filterable_id' => $fid,
'filterable_type' => 'App\Profile',
'filter_type' => 'dm.mute'
]
);
return [200];
}
public function unmute(Request $request)
{
$this->validate($request, [
'id' => 'required'
]);
$fid = $request->input('id');
$pid = $request->user()->profile_id;
$f = UserFilter::whereUserId($pid)
->whereFilterableId($fid)
->whereFilterableType('App\Profile')
->whereFilterType('dm.mute')
->firstOrFail();
$f->delete();
return [200];
}
public function remoteDeliver($dm)
{
$profile = $dm->author;
$url = $dm->recipient->inbox_url;
$tags = [
[
'type' => 'Mention',
'href' => $dm->recipient->permalink(),
'name' => $dm->recipient->emailUrl(),
]
];
$body = [
'@context' => [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
[
'sc' => 'http://schema.org#',
'Hashtag' => 'as:Hashtag',
'sensitive' => 'as:sensitive',
]
],
'id' => $dm->status->permalink(),
'type' => 'Create',
'actor' => $dm->status->profile->permalink(),
'published' => $dm->status->created_at->toAtomString(),
'to' => [$dm->recipient->permalink()],
'cc' => [],
'object' => [
'id' => $dm->status->url(),
'type' => 'Note',
'summary' => null,
'content' => $dm->status->rendered ?? $dm->status->caption,
'inReplyTo' => null,
'published' => $dm->status->created_at->toAtomString(),
'url' => $dm->status->url(),
'attributedTo' => $dm->status->profile->permalink(),
'to' => [$dm->recipient->permalink()],
'cc' => [],
'sensitive' => (bool) $dm->status->is_nsfw,
'attachment' => $dm->status->media()->orderBy('order')->get()->map(function ($media) {
return [
'type' => $media->activityVerb(),
'mediaType' => $media->mime,
'url' => $media->url(),
'name' => $media->caption,
];
})->toArray(),
'tag' => $tags,
]
];
Helpers::sendSignedObject($profile, $url, $body);
}
public function remoteDelete($dm)
{
$profile = $dm->author;
$url = $dm->recipient->inbox_url;
$body = [
'@context' => [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
[
'sc' => 'http://schema.org#',
'Hashtag' => 'as:Hashtag',
'sensitive' => 'as:sensitive',
]
],
'id' => $dm->status->permalink('#delete'),
'to' => [
'https://www.w3.org/ns/activitystreams#Public'
],
'type' => 'Delete',
'actor' => $dm->status->profile->permalink(),
'object' => [
'id' => $dm->status->url(),
'type' => 'Tombstone'
]
];
Helpers::sendSignedObject($profile, $url, $body);
}
}

View File

@ -34,7 +34,8 @@ class FederationController extends Controller
public function nodeinfoWellKnown()
{
abort_if(!config('federation.nodeinfo.enabled'), 404);
return response()->json(Nodeinfo::wellKnown());
return response()->json(Nodeinfo::wellKnown())
->header('Access-Control-Allow-Origin','*');
}
public function nodeinfo()
@ -62,7 +63,8 @@ class FederationController extends Controller
}
$webfinger = (new Webfinger($profile))->generate();
return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT);
return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT)
->header('Access-Control-Allow-Origin','*');
}
public function hostMeta(Request $request)

View File

@ -34,6 +34,7 @@ trait PrivacySettings
$fields = [
'is_private',
'crawlable',
'public_dm',
'show_profile_follower_count',
'show_profile_following_count',
];
@ -56,6 +57,12 @@ trait PrivacySettings
} else {
$settings->{$field} = true;
}
} elseif ($field == 'public_dm') {
if ($form == 'on') {
$settings->{$field} = true;
} else {
$settings->{$field} = false;
}
} else {
if ($form == 'on') {
$settings->{$field} = true;

View File

@ -407,4 +407,9 @@ class Status extends Model
return $this->belongsTo(Place::class);
}
public function directMessage()
{
return $this->hasOne(DirectMessage::class);
}
}

View File

@ -50,6 +50,7 @@ class NotificationTransformer extends Fractal\TransformerAbstract
public function replaceTypeVerb($verb)
{
$verbs = [
'dm' => 'direct',
'follow' => 'follow',
'mention' => 'mention',
'reblog' => 'share',

View File

@ -5,20 +5,25 @@ namespace App\Util\ActivityPub;
use Cache, DB, Log, Purify, Redis, Validator;
use App\{
Activity,
DirectMessage,
Follower,
FollowRequest,
Like,
Notification,
Media,
Profile,
Status,
StatusHashtag,
UserFilter
};
use Carbon\Carbon;
use App\Util\ActivityPub\Helpers;
use Illuminate\Support\Str;
use App\Jobs\LikePipeline\LikePipeline;
use App\Jobs\FollowPipeline\FollowPipeline;
use App\Util\ActivityPub\Validator\Accept as AcceptValidator;
use App\Util\ActivityPub\Validator\Add as AddValidator;
use App\Util\ActivityPub\Validator\Announce as AnnounceValidator;
use App\Util\ActivityPub\Validator\Follow as FollowValidator;
use App\Util\ActivityPub\Validator\Like as LikeValidator;
@ -57,6 +62,12 @@ class Inbox
{
$verb = (string) $this->payload['type'];
switch ($verb) {
case 'Add':
if(AddValidator::validate($this->payload) == false) { return; }
$this->handleAddActivity();
break;
case 'Create':
$this->handleCreateActivity();
break;
@ -121,9 +132,27 @@ class Inbox
return Helpers::profileFetch($actorUrl);
}
public function handleAddActivity()
{
// stories ;)
}
public function handleCreateActivity()
{
$activity = $this->payload['object'];
$actor = $this->actorFirstOrCreate($this->payload['actor']);
if(!$actor || $actor->domain == null) {
return;
}
$to = $activity['to'];
$cc = $activity['cc'];
if(count($to) == 1 &&
count($cc) == 0 &&
parse_url($to[0], PHP_URL_HOST) == config('pixelfed.domain.app')
) {
$this->handleDirectMessage();
return;
}
if(!$this->verifyNoteAttachment()) {
return;
}
@ -170,6 +199,127 @@ class Inbox
return;
}
public function handleDirectMessage()
{
$activity = $this->payload['object'];
$actor = $this->actorFirstOrCreate($this->payload['actor']);
$profile = Profile::whereNull('domain')
->whereUsername(array_last(explode('/', $activity['to'][0])))
->firstOrFail();
if(in_array($actor->id, $profile->blockedIds()->toArray())) {
return;
}
$msg = $activity['content'];
$msgText = strip_tags($activity['content']);
if(Str::startsWith($msgText, '@' . $profile->username)) {
$len = strlen('@' . $profile->username);
$msgText = substr($msgText, $len + 1);
}
if($profile->user->settings->public_dm == false || $profile->is_private) {
if($profile->follows($actor) == true) {
$hidden = false;
} else {
$hidden = true;
}
} else {
$hidden = false;
}
$status = new Status;
$status->profile_id = $actor->id;
$status->caption = $msgText;
$status->rendered = $msg;
$status->visibility = 'direct';
$status->scope = 'direct';
$status->url = $activity['id'];
$status->in_reply_to_profile_id = $profile->id;
$status->save();
$dm = new DirectMessage;
$dm->to_id = $profile->id;
$dm->from_id = $actor->id;
$dm->status_id = $status->id;
$dm->is_hidden = $hidden;
$dm->type = 'text';
$dm->save();
if(count($activity['attachment'])) {
$photos = 0;
$videos = 0;
$allowed = explode(',', config('pixelfed.media_types'));
$activity['attachment'] = array_slice($activity['attachment'], 0, config('pixelfed.max_album_length'));
foreach($activity['attachment'] as $a) {
$type = $a['mediaType'];
$url = $a['url'];
$valid = Helpers::validateUrl($url);
if(in_array($type, $allowed) == false || $valid == false) {
continue;
}
$media = new Media();
$media->remote_media = true;
$media->status_id = $status->id;
$media->profile_id = $status->profile_id;
$media->user_id = null;
$media->media_path = $url;
$media->remote_url = $url;
$media->mime = $type;
$media->save();
if(explode('/', $type)[0] == 'image') {
$photos = $photos + 1;
}
if(explode('/', $type)[0] == 'video') {
$videos = $videos + 1;
}
}
if($photos && $videos == 0) {
$dm->type = $photos == 1 ? 'photo' : 'photos';
$dm->save();
}
if($videos && $photos == 0) {
$dm->type = $videos == 1 ? 'video' : 'videos';
$dm->save();
}
}
if(filter_var($msgText, FILTER_VALIDATE_URL)) {
if(Helpers::validateUrl($msgText)) {
$dm->type = 'link';
$dm->meta = [
'domain' => parse_url($msgText, PHP_URL_HOST),
'local' => parse_url($msgText, PHP_URL_HOST) ==
parse_url(config('app.url'), PHP_URL_HOST)
];
$dm->save();
}
}
$nf = UserFilter::whereUserId($profile->id)
->whereFilterableId($actor->id)
->whereFilterableType('App\Profile')
->whereFilterType('dm.mute')
->exists();
if($profile->domain == null && $hidden == false && !$nf) {
$notification = new Notification();
$notification->profile_id = $profile->id;
$notification->actor_id = $actor->id;
$notification->action = 'dm';
$notification->message = $dm->toText();
$notification->rendered = $dm->toHtml();
$notification->item_id = $dm->id;
$notification->item_type = "App\DirectMessage";
$notification->save();
}
return;
}
public function handleFollowActivity()
{
$actor = $this->actorFirstOrCreate($this->payload['actor']);
@ -305,7 +455,20 @@ class Inbox
}
$actor = $this->payload['actor'];
$obj = $this->payload['object'];
if(is_string($obj) == true) {
if(is_string($obj) == true && $actor == $obj && Helpers::validateUrl($obj)) {
$profile = Profile::whereRemoteUrl($obj)->first();
if(!$profile || $profile->private_key != null) {
return;
}
Notification::whereActorId($profile->id)->delete();
$profile->avatar()->delete();
$profile->followers()->delete();
$profile->following()->delete();
$profile->likes()->delete();
$profile->media()->delete();
$profile->hashtags()->delete();
$profile->statuses()->delete();
$profile->delete();
return;
}
$type = $this->payload['object']['type'];
@ -319,9 +482,7 @@ class Inbox
$id = $this->payload['object']['id'];
switch ($type) {
case 'Person':
// todo: fix race condition
return;
$profile = Helpers::profileFetch($actor);
$profile = Profile::whereRemoteUrl($actor)->first();
if(!$profile || $profile->private_key != null) {
return;
}
@ -331,6 +492,7 @@ class Inbox
$profile->following()->delete();
$profile->likes()->delete();
$profile->media()->delete();
$profile->hashtags()->delete();
$profile->statuses()->delete();
$profile->delete();
return;
@ -346,6 +508,7 @@ class Inbox
if(!$status) {
return;
}
$status->directMessage()->delete();
$status->media()->delete();
$status->likes()->delete();
$status->shares()->delete();

View File

@ -0,0 +1,41 @@
<?php
namespace App\Util\ActivityPub\Validator;
use Validator;
use Illuminate\Validation\Rule;
class Add {
public static function validate($payload)
{
$valid = Validator::make($payload, [
'@context' => 'required',
'id' => 'required|string',
'type' => [
'required',
Rule::in(['Add'])
],
'actor' => 'required|url',
'object' => 'required',
'object.id' => 'required|url',
'object.type' => [
'required',
Rule::in(['Story'])
],
'object.attributedTo' => 'required|url|same:actor',
'object.attachment' => 'required',
'object.attachment.type' => [
'required',
Rule::in(['Image'])
],
'object.attachment.url' => 'required|url',
'object.attachment.mediaType' => [
'required',
Rule::in(['image/jpeg', 'image/png'])
]
])->passes();
return $valid;
}
}

View File

@ -4,19 +4,23 @@ namespace App\Util\Lexer;
class PrettyNumber
{
public static function convert($expression)
public static function convert($number)
{
if(!is_integer($number)) {
return $number;
}
$abbrevs = [12 => 'T', 9 => 'B', 6 => 'M', 3 => 'K', 0 => ''];
foreach ($abbrevs as $exponent => $abbrev) {
if ($expression >= pow(10, $exponent)) {
$display_num = $expression / pow(10, $exponent);
$num = number_format($display_num, 0).$abbrev;
return $num;
if(abs($number) >= pow(10, $exponent)) {
$display = $number / pow(10, $exponent);
$decimals = ($exponent >= 3 && round($display) < 100) ? 1 : 0;
$number = number_format($display, $decimals).$abbrev;
break;
}
}
return $expression;
return $number;
}
public static function size($expression, $kb = false)

View File

@ -165,6 +165,8 @@ class Image
$quality = config('pixelfed.image_quality');
$img->save($newPath, $quality);
$media->width = $img->width();
$media->height = $img->height();
$img->destroy();
if (!$thumbnail) {
$media->orientation = $orientation;
@ -178,6 +180,7 @@ class Image
$media->mime = $img->mime;
}
$media->save();
Cache::forget('status:transformer:media:attachments:'.$media->status_id);
Cache::forget('status:thumb:'.$media->status_id);

View File

@ -21,7 +21,9 @@ return [
* You can enable CORS for 1 or multiple paths.
* Example: ['api/*']
*/
'paths' => [],
'paths' => [
'.well-known/*'
],
/*
* Matches the request method. `[*]` allows all methods.

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddTypeToDirectMessagesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('direct_messages', function (Blueprint $table) {
$table->string('type')->default('text')->nullable()->index()->after('from_id');
$table->boolean('is_hidden')->default(false)->index()->after('group_message');
$table->json('meta')->nullable()->after('is_hidden');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('direct_messages', function (Blueprint $table) {
$table->dropColumn('type');
$table->dropColumn('is_hidden');
$table->dropColumn('meta');
});
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/direct.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/loops.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([[16],{19:function(t,e,s){t.exports=s("ETg6")},"7wkd":function(t,e,s){"use strict";s.r(e);var o={data:function(){return{loaded:!1,showLoadMore:!0,profiles:[],page:1}},beforeMount:function(){this.fetchData()},methods:{fetchData:function(){var t=this;axios.get("/api/pixelfed/v2/discover/profiles",{params:{page:this.page}}).then((function(e){if(0==e.data.length)return t.showLoadMore=!1,void(t.loaded=!0);t.profiles=e.data,t.showLoadMore=8==t.profiles.length,t.loaded=!0}))},prettyCount:function(t){return App.util.format.count(t)},loadMore:function(){this.loaded=!1,this.page++,this.fetchData()},thumbUrl:function(t){return t.media_attachments[0].url}}},a=s("KHd+"),n=Object(a.a)(o,(function(){var t=this,e=t.$createElement,s=t._self._c||e;return s("div",[s("div",{staticClass:"col-12"},[s("p",{staticClass:"font-weight-bold text-lighter text-uppercase"},[t._v("Profiles Directory")]),t._v(" "),t.loaded?s("div",{},[s("div",{staticClass:"row"},[t._l(t.profiles,(function(e,o){return s("div",{staticClass:"col-12 col-md-6 p-1"},[s("div",{staticClass:"card card-body border shadow-none py-2"},[s("div",{staticClass:"media"},[s("a",{attrs:{href:e.url}},[s("img",{staticClass:"rounded-circle border mr-3",attrs:{src:e.avatar,alt:"...",width:"40px",height:"40px"}})]),t._v(" "),s("div",{staticClass:"media-body"},[s("p",{staticClass:"mt-0 mb-0 font-weight-bold"},[s("a",{staticClass:"text-dark",attrs:{href:e.url}},[t._v(t._s(e.username))])]),t._v(" "),s("p",{staticClass:"mb-1 small text-lighter d-flex justify-content-between font-weight-bold"},[s("span",[s("span",[t._v(t._s(t.prettyCount(e.statuses_count)))]),t._v(" POSTS\n\t\t\t\t\t\t\t\t\t")]),t._v(" "),s("span",[s("span",[t._v(t._s(t.prettyCount(e.followers_count)))]),t._v(" FOLLOWERS\n\t\t\t\t\t\t\t\t\t")])]),t._v(" "),s("p",{staticClass:"mb-1"},t._l(e.posts,(function(e,o){return s("span",{key:"profile_posts_"+o,staticClass:"shadow-sm"},[s("a",{staticClass:"text-decoration-none mr-1",attrs:{href:e.url}},[s("img",{staticClass:"border rounded",attrs:{src:t.thumbUrl(e),width:"62.3px",height:"62.3px"}})])])})),0)])])])])})),t._v(" "),t.showLoadMore?s("div",{staticClass:"col-12"},[s("p",{staticClass:"text-center mb-0 pt-3"},[s("button",{staticClass:"btn btn-outline-secondary btn-sm px-4 py-1 font-weight-bold",on:{click:function(e){return t.loadMore()}}},[t._v("Load More")])])]):t._e()],2)]):s("div",[t._m(0)])])])}),[function(){var t=this.$createElement,e=this._self._c||t;return e("div",{staticClass:"row"},[e("div",{staticClass:"col-12 d-flex justify-content-center align-items-center"},[e("div",{staticClass:"spinner-border",attrs:{role:"status"}},[e("span",{staticClass:"sr-only"},[this._v("Loading...")])])])])}],!1,null,"7b3eea1c",null);e.default=n.exports},ETg6:function(t,e,s){Vue.component("profile-directory",s("7wkd").default)},"KHd+":function(t,e,s){"use strict";function o(t,e,s,o,a,n,r,i){var c,d="function"==typeof t?t.options:t;if(e&&(d.render=e,d.staticRenderFns=s,d._compiled=!0),o&&(d.functional=!0),n&&(d._scopeId="data-v-"+n),r?(c=function(t){(t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),a&&a.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(r)},d._ssrRegister=c):a&&(c=i?function(){a.call(this,this.$root.$options.shadowRoot)}:a),c)if(d.functional){d._injectStyles=c;var l=d.render;d.render=function(t,e){return c.call(e),l(t,e)}}else{var u=d.beforeCreate;d.beforeCreate=u?[].concat(u,c):[c]}return{exports:t,options:d}}s.d(e,"a",(function(){return o}))}},[[19,0]]]);
(window.webpackJsonp=window.webpackJsonp||[]).push([[17],{19:function(t,e,s){t.exports=s("ETg6")},"7wkd":function(t,e,s){"use strict";s.r(e);var o={data:function(){return{loaded:!1,showLoadMore:!0,profiles:[],page:1}},beforeMount:function(){this.fetchData()},methods:{fetchData:function(){var t=this;axios.get("/api/pixelfed/v2/discover/profiles",{params:{page:this.page}}).then((function(e){if(0==e.data.length)return t.showLoadMore=!1,void(t.loaded=!0);t.profiles=e.data,t.showLoadMore=8==t.profiles.length,t.loaded=!0}))},prettyCount:function(t){return App.util.format.count(t)},loadMore:function(){this.loaded=!1,this.page++,this.fetchData()},thumbUrl:function(t){return t.media_attachments[0].url}}},a=s("KHd+"),n=Object(a.a)(o,(function(){var t=this,e=t.$createElement,s=t._self._c||e;return s("div",[s("div",{staticClass:"col-12"},[s("p",{staticClass:"font-weight-bold text-lighter text-uppercase"},[t._v("Profiles Directory")]),t._v(" "),t.loaded?s("div",{},[s("div",{staticClass:"row"},[t._l(t.profiles,(function(e,o){return s("div",{staticClass:"col-12 col-md-6 p-1"},[s("div",{staticClass:"card card-body border shadow-none py-2"},[s("div",{staticClass:"media"},[s("a",{attrs:{href:e.url}},[s("img",{staticClass:"rounded-circle border mr-3",attrs:{src:e.avatar,alt:"...",width:"40px",height:"40px"}})]),t._v(" "),s("div",{staticClass:"media-body"},[s("p",{staticClass:"mt-0 mb-0 font-weight-bold"},[s("a",{staticClass:"text-dark",attrs:{href:e.url}},[t._v(t._s(e.username))])]),t._v(" "),s("p",{staticClass:"mb-1 small text-lighter d-flex justify-content-between font-weight-bold"},[s("span",[s("span",[t._v(t._s(t.prettyCount(e.statuses_count)))]),t._v(" POSTS\n\t\t\t\t\t\t\t\t\t")]),t._v(" "),s("span",[s("span",[t._v(t._s(t.prettyCount(e.followers_count)))]),t._v(" FOLLOWERS\n\t\t\t\t\t\t\t\t\t")])]),t._v(" "),s("p",{staticClass:"mb-1"},t._l(e.posts,(function(e,o){return s("span",{key:"profile_posts_"+o,staticClass:"shadow-sm"},[s("a",{staticClass:"text-decoration-none mr-1",attrs:{href:e.url}},[s("img",{staticClass:"border rounded",attrs:{src:t.thumbUrl(e),width:"62.3px",height:"62.3px"}})])])})),0)])])])])})),t._v(" "),t.showLoadMore?s("div",{staticClass:"col-12"},[s("p",{staticClass:"text-center mb-0 pt-3"},[s("button",{staticClass:"btn btn-outline-secondary btn-sm px-4 py-1 font-weight-bold",on:{click:function(e){return t.loadMore()}}},[t._v("Load More")])])]):t._e()],2)]):s("div",[t._m(0)])])])}),[function(){var t=this.$createElement,e=this._self._c||t;return e("div",{staticClass:"row"},[e("div",{staticClass:"col-12 d-flex justify-content-center align-items-center"},[e("div",{staticClass:"spinner-border",attrs:{role:"status"}},[e("span",{staticClass:"sr-only"},[this._v("Loading...")])])])])}],!1,null,"7b3eea1c",null);e.default=n.exports},ETg6:function(t,e,s){Vue.component("profile-directory",s("7wkd").default)},"KHd+":function(t,e,s){"use strict";function o(t,e,s,o,a,n,r,i){var c,d="function"==typeof t?t.options:t;if(e&&(d.render=e,d.staticRenderFns=s,d._compiled=!0),o&&(d.functional=!0),n&&(d._scopeId="data-v-"+n),r?(c=function(t){(t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),a&&a.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(r)},d._ssrRegister=c):a&&(c=i?function(){a.call(this,this.$root.$options.shadowRoot)}:a),c)if(d.functional){d._injectStyles=c;var l=d.render;d.render=function(t,e){return c.call(e),l(t,e)}}else{var u=d.beforeCreate;d.beforeCreate=u?[].concat(u,c):[c]}return{exports:t,options:d}}s.d(e,"a",(function(){return o}))}},[[19,0]]]);

File diff suppressed because one or more lines are too long

2
public/js/quill.js vendored

File diff suppressed because one or more lines are too long

2
public/js/rempos.js vendored

File diff suppressed because one or more lines are too long

2
public/js/rempro.js vendored

File diff suppressed because one or more lines are too long

2
public/js/search.js vendored

File diff suppressed because one or more lines are too long

2
public/js/status.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([[23],{15:function(e,a,o){e.exports=o("YMO/")},"YMO/":function(e,a,o){(function(e){function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}ace.define("ace/theme/monokai",["require","exports","module","ace/lib/dom"],(function(e,a,o){a.isDark=!0,a.cssClass="ace-monokai",a.cssText=".ace-monokai .ace_gutter {background: #2F3129;color: #8F908A}.ace-monokai .ace_print-margin {width: 1px;background: #555651}.ace-monokai {background-color: #272822;color: #F8F8F2}.ace-monokai .ace_cursor {color: #F8F8F0}.ace-monokai .ace_marker-layer .ace_selection {background: #49483E}.ace-monokai.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #272822;}.ace-monokai .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-monokai .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #49483E}.ace-monokai .ace_marker-layer .ace_active-line {background: #202020}.ace-monokai .ace_gutter-active-line {background-color: #272727}.ace-monokai .ace_marker-layer .ace_selected-word {border: 1px solid #49483E}.ace-monokai .ace_invisible {color: #52524d}.ace-monokai .ace_entity.ace_name.ace_tag,.ace-monokai .ace_keyword,.ace-monokai .ace_meta.ace_tag,.ace-monokai .ace_storage {color: #F92672}.ace-monokai .ace_punctuation,.ace-monokai .ace_punctuation.ace_tag {color: #fff}.ace-monokai .ace_constant.ace_character,.ace-monokai .ace_constant.ace_language,.ace-monokai .ace_constant.ace_numeric,.ace-monokai .ace_constant.ace_other {color: #AE81FF}.ace-monokai .ace_invalid {color: #F8F8F0;background-color: #F92672}.ace-monokai .ace_invalid.ace_deprecated {color: #F8F8F0;background-color: #AE81FF}.ace-monokai .ace_support.ace_constant,.ace-monokai .ace_support.ace_function {color: #66D9EF}.ace-monokai .ace_fold {background-color: #A6E22E;border-color: #F8F8F2}.ace-monokai .ace_storage.ace_type,.ace-monokai .ace_support.ace_class,.ace-monokai .ace_support.ace_type {font-style: italic;color: #66D9EF}.ace-monokai .ace_entity.ace_name.ace_function,.ace-monokai .ace_entity.ace_other,.ace-monokai .ace_entity.ace_other.ace_attribute-name,.ace-monokai .ace_variable {color: #A6E22E}.ace-monokai .ace_variable.ace_parameter {font-style: italic;color: #FD971F}.ace-monokai .ace_string {color: #E6DB74}.ace-monokai .ace_comment {color: #75715E}.ace-monokai .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ0FD0ZXBzd/wPAAjVAoxeSgNeAAAAAElFTkSuQmCC) right repeat-y}",e("../lib/dom").importCssString(a.cssText,a.cssClass)})),ace.require(["ace/theme/monokai"],(function(c){"object"==o(e)&&"object"==o(a)&&e&&(e.exports=c)}))}).call(this,o("YuTi")(e))},YuTi:function(e,a){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children||(e.children=[]),Object.defineProperty(e,"loaded",{enumerable:!0,get:function(){return e.l}}),Object.defineProperty(e,"id",{enumerable:!0,get:function(){return e.i}}),e.webpackPolyfill=1),e}}},[[15,0]]]);
(window.webpackJsonp=window.webpackJsonp||[]).push([[24],{15:function(e,a,o){e.exports=o("YMO/")},"YMO/":function(e,a,o){(function(e){function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}ace.define("ace/theme/monokai",["require","exports","module","ace/lib/dom"],(function(e,a,o){a.isDark=!0,a.cssClass="ace-monokai",a.cssText=".ace-monokai .ace_gutter {background: #2F3129;color: #8F908A}.ace-monokai .ace_print-margin {width: 1px;background: #555651}.ace-monokai {background-color: #272822;color: #F8F8F2}.ace-monokai .ace_cursor {color: #F8F8F0}.ace-monokai .ace_marker-layer .ace_selection {background: #49483E}.ace-monokai.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #272822;}.ace-monokai .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-monokai .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #49483E}.ace-monokai .ace_marker-layer .ace_active-line {background: #202020}.ace-monokai .ace_gutter-active-line {background-color: #272727}.ace-monokai .ace_marker-layer .ace_selected-word {border: 1px solid #49483E}.ace-monokai .ace_invisible {color: #52524d}.ace-monokai .ace_entity.ace_name.ace_tag,.ace-monokai .ace_keyword,.ace-monokai .ace_meta.ace_tag,.ace-monokai .ace_storage {color: #F92672}.ace-monokai .ace_punctuation,.ace-monokai .ace_punctuation.ace_tag {color: #fff}.ace-monokai .ace_constant.ace_character,.ace-monokai .ace_constant.ace_language,.ace-monokai .ace_constant.ace_numeric,.ace-monokai .ace_constant.ace_other {color: #AE81FF}.ace-monokai .ace_invalid {color: #F8F8F0;background-color: #F92672}.ace-monokai .ace_invalid.ace_deprecated {color: #F8F8F0;background-color: #AE81FF}.ace-monokai .ace_support.ace_constant,.ace-monokai .ace_support.ace_function {color: #66D9EF}.ace-monokai .ace_fold {background-color: #A6E22E;border-color: #F8F8F2}.ace-monokai .ace_storage.ace_type,.ace-monokai .ace_support.ace_class,.ace-monokai .ace_support.ace_type {font-style: italic;color: #66D9EF}.ace-monokai .ace_entity.ace_name.ace_function,.ace-monokai .ace_entity.ace_other,.ace-monokai .ace_entity.ace_other.ace_attribute-name,.ace-monokai .ace_variable {color: #A6E22E}.ace-monokai .ace_variable.ace_parameter {font-style: italic;color: #FD971F}.ace-monokai .ace_string {color: #E6DB74}.ace-monokai .ace_comment {color: #75715E}.ace-monokai .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ0FD0ZXBzd/wPAAjVAoxeSgNeAAAAAElFTkSuQmCC) right repeat-y}",e("../lib/dom").importCssString(a.cssText,a.cssClass)})),ace.require(["ace/theme/monokai"],(function(c){"object"==o(e)&&"object"==o(a)&&e&&(e.exports=c)}))}).call(this,o("YuTi")(e))},YuTi:function(e,a){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children||(e.children=[]),Object.defineProperty(e,"loaded",{enumerable:!0,get:function(){return e.l}}),Object.defineProperty(e,"id",{enumerable:!0,get:function(){return e.i}}),e.webpackPolyfill=1),e}}},[[15,0]]]);

File diff suppressed because one or more lines are too long

2
public/js/vendor.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,8 @@
{
"/js/manifest.js": "/js/manifest.js?id=7db827d654313dce4250",
"/js/vendor.js": "/js/vendor.js?id=0b69f538b56102c1b0f9",
"/js/vendor.js": "/js/vendor.js?id=d6ba75064c2942474259",
"/js/ace.js": "/js/ace.js?id=11e5550a450fece75c33",
"/js/activity.js": "/js/activity.js?id=69fa5dc5355caa260a13",
"/js/activity.js": "/js/activity.js?id=838e242ddbe91e559f00",
"/js/app.js": "/js/app.js?id=d1274e1bbd0cccd6089f",
"/css/app.css": "/css/app.css?id=77729cabd5c8a0ad09b8",
"/css/appdark.css": "/css/appdark.css?id=e608aa1d95e1c8ed3060",
@ -11,21 +11,22 @@
"/js/collectioncompose.js": "/js/collectioncompose.js?id=3fd79944492361ec7347",
"/js/collections.js": "/js/collections.js?id=38be4150f3d2ebb15f50",
"/js/components.js": "/js/components.js?id=88296701f1382d285031",
"/js/compose.js": "/js/compose.js?id=a835e925552e7a2f0709",
"/js/compose.js": "/js/compose.js?id=bd3e195dc7cf1ed0cd33",
"/js/compose-classic.js": "/js/compose-classic.js?id=283f19c895f4118a2a8b",
"/js/developers.js": "/js/developers.js?id=f75deca5ccf47d43eb07",
"/js/discover.js": "/js/discover.js?id=57595c456e15bf75f71d",
"/js/hashtag.js": "/js/hashtag.js?id=7e57442cbe641b10bfae",
"/js/loops.js": "/js/loops.js?id=ac610897b12207c829b9",
"/js/mode-dot.js": "/js/mode-dot.js?id=1225a9aac7a93d5d232f",
"/js/profile.js": "/js/profile.js?id=9bdaef57acf15336d2a8",
"/js/profile-directory.js": "/js/profile-directory.js?id=611af669221ad8be3068",
"/js/quill.js": "/js/quill.js?id=00f2dca2463b3c9d3920",
"/js/rempos.js": "/js/rempos.js?id=f7c284ac260f4f9b9474",
"/js/rempro.js": "/js/rempro.js?id=bfe52f1149330c486da7",
"/js/search.js": "/js/search.js?id=0c1e20a004ada75853ec",
"/js/status.js": "/js/status.js?id=8abf06cc5bd0becad543",
"/js/story-compose.js": "/js/story-compose.js?id=8768fd0f62554e98ef10",
"/js/theme-monokai.js": "/js/theme-monokai.js?id=3b6e62701f12b717cc5c",
"/js/timeline.js": "/js/timeline.js?id=df7947354d3da37f61e3"
"/js/direct.js": "/js/direct.js?id=ffc75ccefaaea359dab5",
"/js/discover.js": "/js/discover.js?id=6d88f29999bdd651ec35",
"/js/hashtag.js": "/js/hashtag.js?id=eb4ce037114cd78a8678",
"/js/loops.js": "/js/loops.js?id=1dcb3790eb9ea4ea5848",
"/js/mode-dot.js": "/js/mode-dot.js?id=d54ad862baf30ee756f8",
"/js/profile.js": "/js/profile.js?id=746f776639e98cd4102a",
"/js/profile-directory.js": "/js/profile-directory.js?id=3a3be60e979308f9958e",
"/js/quill.js": "/js/quill.js?id=e47c5b9f47415104bb6a",
"/js/rempos.js": "/js/rempos.js?id=0c655b6b8dca92d06c79",
"/js/rempro.js": "/js/rempro.js?id=d26f22f402cd6aab5f20",
"/js/search.js": "/js/search.js?id=2af4d1de55be62523b71",
"/js/status.js": "/js/status.js?id=b744f13cca28d1e9964e",
"/js/story-compose.js": "/js/story-compose.js?id=6308c4b7cb5baa4517d3",
"/js/theme-monokai.js": "/js/theme-monokai.js?id=ece67a5ba28761df4d88",
"/js/timeline.js": "/js/timeline.js?id=8e47506fff6751b8647f"
}

View File

@ -52,6 +52,11 @@
<a :href="n.account.url" class="font-weight-bold text-dark word-break" :title="n.account.username">{{truncate(n.account.username)}}</a> tagged you in a <a class="font-weight-bold" v-bind:href="n.tagged.post_url">post</a>.
</p>
</div>
<div v-else-if="n.type == 'direct'">
<p class="my-0">
<a :href="n.account.url" class="font-weight-bold text-dark word-break" :title="n.account.username">{{truncate(n.account.username)}}</a> sent a <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">dm</a>.
</p>
</div>
<div class="align-items-center">
<span class="small text-muted" data-toggle="tooltip" data-placement="bottom" :title="n.created_at">{{timeAgo(n.created_at)}}</span>
</div>
@ -249,6 +254,9 @@ export default {
case 'tagged':
return n.tagged.post_url;
break;
case 'direct':
return '/account/direct/t/'+n.account.id;
break
}
return '/';
},

View File

@ -237,7 +237,7 @@
<div class="media-body">
<div class="form-group">
<label class="font-weight-bold text-muted small d-none">Caption</label>
<textarea class="form-control border-0 rounded-0 no-focus" rows="2" placeholder="Write a caption..." style="resize:none" v-model="composeText" v-on:keyup="composeTextLength = composeText.length"></textarea>
<textarea class="form-control border-0 rounded-0 no-focus" rows="3" placeholder="Write a caption..." style="" v-model="composeText" v-on:keyup="composeTextLength = composeText.length"></textarea>
<p class="help-text small text-right text-muted mb-0">{{composeTextLength}}/{{config.uploader.max_caption_length}}</p>
</div>
</div>
@ -271,7 +271,7 @@
<p class="px-4 mb-0 py-2">
<span>Audience</span>
<span class="float-right">
<a v-if="profile.locked == false" href="#" @click.prevent="showVisibilityCard()" class="btn btn-outline-secondary btn-sm small mr-3 mt-n1 disabled" style="font-size:10px;padding:3px;text-transform: uppercase" disabled>{{visibilityTag}}</a>
<a href="#" @click.prevent="showVisibilityCard()" class="btn btn-outline-secondary btn-sm small mr-3 mt-n1 disabled" style="font-size:10px;padding:3px;text-transform: uppercase" disabled>{{visibilityTag}}</a>
<a href="#" @click.prevent="showVisibilityCard()" class="text-decoration-none"><i class="fas fa-chevron-right fa-lg text-lighter"></i></a>
</span>
</p>
@ -632,12 +632,13 @@ export default {
methods: {
fetchProfile() {
let self = this;
axios.get('/api/pixelfed/v1/accounts/verify_credentials').then(res => {
this.profile = res.data;
self.profile = res.data;
window.pixelfed.currentUser = res.data;
if(res.data.locked == true) {
this.visibility = 'private';
this.visibilityTag = 'Followers Only';
self.visibility = 'private';
self.visibilityTag = 'Followers Only';
}
}).catch(err => {
});
@ -663,6 +664,9 @@ export default {
let self = this;
self.uploading = true;
let io = document.querySelector('#pf-dz');
if(!io.files.length) {
self.uploading = false;
}
Array.prototype.forEach.call(io.files, function(io, i) {
if(self.media && self.media.length + i >= self.config.uploader.album_limit) {
swal('Error', 'You can only upload ' + self.config.uploader.album_limit + ' photos per album', 'error');

View File

@ -0,0 +1,375 @@
<template>
<div>
<div v-if="loaded && page == 'browse'" class="container messages-page p-0 p-md-2 mt-n4" style="min-height: 50vh;">
<div class="col-12 col-md-8 offset-md-2 p-0 px-md-2">
<div class="card shadow-none border mt-4">
<div class="card-header bg-white py-4">
<span class="h4 font-weight-bold mb-0">Direct Messages</span>
<span class="float-right">
<a class="btn btn-outline-primary font-weight-bold py-0 rounded-pill" href="#" @click.prevent="goto('add')">New Message</a>
</span>
</div>
<div class="card-header bg-white">
<ul class="nav nav-pills nav-fill">
<li class="nav-item">
<a :class="[tab == 'inbox' ? 'nav-link px-4 font-weight-bold rounded-pill active' : 'nav-link px-4 font-weight-bold rounded-pill']" @click.prevent="switchTab('inbox')" href="#">Inbox</a>
</li>
<li class="nav-item">
<a :class="[tab == 'sent' ? 'nav-link px-4 font-weight-bold rounded-pill active' : 'nav-link px-4 font-weight-bold rounded-pill']" @click.prevent="switchTab('sent')" href="#">Sent</a>
</li>
<li class="nav-item">
<a :class="[tab == 'filtered' ? 'nav-link px-4 font-weight-bold rounded-pill active' : 'nav-link px-4 font-weight-bold rounded-pill']" @click.prevent="switchTab('filtered')" href="#">Filtered</a>
</li>
</ul>
</div>
<ul v-if="tab == 'inbox'" class="list-group list-group-flush">
<div v-if="!messages.inbox.length" class="list-group-item d-flex justify-content-center align-items-center" style="min-height: 40vh;">
<p class="lead mb-0">No messages found :(</p>
</div>
<div v-else v-for="(thread, index) in messages.inbox">
<a class="list-group-item text-dark text-decoration-none border-left-0 border-right-0 border-top-0" :href="'/account/direct/t/'+thread.id">
<div class="media d-flex align-items-center">
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" alt="Generic placeholder image" width="32px">
<div class="media-body">
<p class="mb-0">
<span class="font-weight-bold text-truncate">
{{thread.name}}
</span>
<span class="pl-1 text-muted small text-truncate" style="font-weight: 500;">
{{thread.isLocal ? '@' + thread.username : thread.username}}
</span>
</p>
<p class="text-muted mb-0" style="font-size:13px;font-weight: 500;">
<span>
<i class="far fa-comment text-primary"></i>
</span>
<span class="pl-1 pr-3">
Received
</span>
<span>
{{thread.timeAgo}}
</span>
</p>
</div>
<span class="float-right">
<i class="fas fa-chevron-right fa-lg text-lighter"></i>
</span>
</div>
</a>
</div>
</ul>
<ul v-if="tab == 'sent'" class="list-group list-group-flush">
<div v-if="!messages.sent.length" class="list-group-item d-flex justify-content-center align-items-center" style="min-height: 40vh;">
<p class="lead mb-0">No messages found :(</p>
</div>
<div v-else v-for="(thread, index) in messages.sent">
<a class="list-group-item text-dark text-decoration-none border-left-0 border-right-0 border-top-0" href="#" @click.prevent="loadMessage(thread.id)">
<div class="media d-flex align-items-center">
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" alt="Generic placeholder image" width="32px">
<div class="media-body">
<p class="mb-0">
<span class="font-weight-bold text-truncate">
{{thread.name}}
</span>
<span class="pl-1 text-muted small text-truncate" style="font-weight: 500;">
{{thread.isLocal ? '@' + thread.username : thread.username}}
</span>
</p>
<p class="text-muted mb-0" style="font-size:13px;font-weight: 500;">
<span>
<i class="far fa-paper-plane text-primary"></i>
</span>
<span class="pl-1 pr-3">
Delivered
</span>
<span>
{{thread.timeAgo}}
</span>
</p>
</div>
<span class="float-right">
<i class="fas fa-chevron-right fa-lg text-lighter"></i>
</span>
</div>
</a>
</div>
</ul>
<ul v-if="tab == 'filtered'" class="list-group list-group-flush">
<div v-if="!messages.filtered.length" class="list-group-item d-flex justify-content-center align-items-center" style="min-height: 40vh;">
<p class="lead mb-0">No messages found :(</p>
</div>
<div v-else v-for="(thread, index) in messages.filtered">
<a class="list-group-item text-dark text-decoration-none border-left-0 border-right-0 border-top-0" href="#" @click.prevent="loadMessage(thread.id)">
<div class="media d-flex align-items-center">
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" alt="Generic placeholder image" width="32px">
<div class="media-body">
<p class="mb-0">
<span class="font-weight-bold text-truncate">
{{thread.name}}
</span>
<span class="pl-1 text-muted small text-truncate" style="font-weight: 500;">
{{thread.isLocal ? '@' + thread.username : thread.username}}
</span>
</p>
<p class="text-muted mb-0" style="font-size:13px;font-weight: 500;">
<span>
<i class="fas fa-shield-alt" style="color:#fd9426"></i>
</span>
<span class="pl-1 pr-3">
Filtered
</span>
<span>
{{thread.timeAgo}}
</span>
</p>
</div>
<span class="float-right">
<i class="fas fa-chevron-right fa-lg text-lighter"></i>
</span>
</div>
</a>
</div>
</ul>
</div>
<div v-if="tab == 'inbox'" class="mt-3 text-center">
<button class="btn btn-outline-primary rounded-pill btn-sm" :disabled="inboxPage == 1" @click="messagePagination('inbox', 'prev')">Prev</button>
<button class="btn btn-outline-primary rounded-pill btn-sm" :disabled="messages.inbox.length != 8" @click="messagePagination('inbox', 'next')">Next</button>
</div>
<div v-if="tab == 'sent'" class="mt-3 text-center">
<button class="btn btn-outline-primary rounded-pill btn-sm" :disabled="sentPage == 1" @click="messagePagination('sent', 'prev')">Prev</button>
<button class="btn btn-outline-primary rounded-pill btn-sm" :disabled="messages.sent.length != 8" @click="messagePagination('sent', 'next')">Next</button>
</div>
<div v-if="tab == 'filtered'" class="mt-3 text-center">
<button class="btn btn-outline-primary rounded-pill btn-sm" :disabled="filteredPage == 1" @click="messagePagination('filtered', 'prev')">Prev</button>
<button class="btn btn-outline-primary rounded-pill btn-sm" :disabled="messages.filtered.length != 8" @click="messagePagination('filtered', 'next')">Next</button>
</div>
</div>
</div>
<div v-if="loaded && page == 'add'" class="container messages-page p-0 p-md-2 mt-n4" style="min-height: 60vh;">
<div class="col-12 col-md-8 offset-md-2 p-0 px-md-2">
<div class="card shadow-none border mt-4">
<div class="card-header bg-white py-4 d-flex justify-content-between">
<span class="cursor-pointer px-3" @click="goto('browse')"><i class="fas fa-chevron-left"></i></span>
<span class="h4 font-weight-bold mb-0">New Direct Message</span>
<span><i class="fas fa-chevron-right text-white"></i></span>
</div>
<div class="card-body d-flex align-items-center justify-content-center" style="height: 60vh;">
<div>
<p class="mb-0 font-weight-bold">Select Recipient</p>
<autocomplete
:search="composeSearch"
:disabled="composeLoading"
placeholder="@dansup"
aria-label="Search usernames"
:get-result-value="getTagResultValue"
@submit="onTagSubmitLocation"
ref="autocomplete"
>
</autocomplete>
<div style="width:300px;"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style type="text/css" scoped>
</style>
<script type="text/javascript">
import Autocomplete from '@trevoreyre/autocomplete-vue'
import '@trevoreyre/autocomplete-vue/dist/style.css'
export default {
components: {
Autocomplete
},
data() {
return {
config: window.App.config,
loaded: false,
profile: {},
page: 'browse',
pages: ['browse', 'add', 'read'],
tab: 'inbox',
tabs: ['inbox', 'sent', 'filtered'],
inboxPage: 1,
sentPage: 1,
filteredPage: 1,
threads: [],
thread: false,
threadIndex: false,
replyText: '',
composeUsername: '',
ctxContext: null,
ctxIndex: null,
uploading: false,
uploadProgress: null,
messages: {
inbox: [],
sent: [],
filtered: []
},
newType: 'select',
composeLoading: false,
}
},
mounted() {
this.fetchProfile();
let self = this;
axios.get('/api/direct/browse', {
params: {
a: 'inbox'
}
})
.then(res => {
self.loaded = true;
this.threads = res.data
this.messages.inbox = res.data;
});
},
updated() {
$('[data-toggle="tooltip"]').tooltip();
},
methods: {
fetchProfile() {
axios.get('/api/pixelfed/v1/accounts/verify_credentials').then(res => {
this.profile = res.data;
window._sharedData.curUser = res.data;
});
},
goto(l = 'browse') {
this.page = l;
},
loadMessage(id) {
let url = '/account/direct/t/' + id;
window.location.href = url;
return;
},
truncate(t) {
return _.truncate(t);
},
switchTab(tab) {
let self = this;
switch(tab) {
case 'inbox':
if(this.messages.inbox.length == 0) {
// fetch
}
break;
case 'sent':
if(this.messages.sent.length == 0) {
axios.get('/api/direct/browse', {
params: {
a: 'sent'
}
})
.then(res => {
self.loaded = true;
self.threads = res.data
self.messages.sent = res.data;
});
}
break;
case 'filtered':
if(this.messages.filtered.length == 0) {
axios.get('/api/direct/browse', {
params: {
a: 'filtered'
}
})
.then(res => {
self.loaded = true;
self.threads = res.data
self.messages.filtered = res.data;
});
}
break;
}
this.tab = tab;
},
composeSearch(input) {
if (input.length < 1) { return []; };
let self = this;
let results = [];
return axios.post('/api/direct/lookup', {
q: input
}).then(res => {
return res.data;
});
},
getTagResultValue(result) {
// return '@' + result.name;
return result.local ? '@' + result.name : result.name;
},
onTagSubmitLocation(result) {
//this.$refs.autocomplete.value = '';
this.composeLoading = true;
window.location.href = '/account/direct/t/' + result.id;
return;
},
messagePagination(tab, dir) {
if(tab == 'inbox') {
this.inboxPage = dir == 'prev' ? this.inboxPage - 1 : this.inboxPage + 1;
axios.get('/api/direct/browse', {
params: {
a: 'inbox',
page: this.inboxPage
}
})
.then(res => {
self.loaded = true;
this.threads = res.data
this.messages.inbox = res.data;
});
}
if(tab == 'sent') {
this.sentPage = dir == 'prev' ? this.sentPage - 1 : this.sentPage + 1;
axios.get('/api/direct/browse', {
params: {
a: 'sent',
page: this.sentPage
}
})
.then(res => {
self.loaded = true;
this.threads = res.data
this.messages.sent = res.data;
});
}
if(tab == 'filtered') {
this.filteredPage = dir == 'prev' ? this.filteredPage - 1 : this.filteredPage + 1;
axios.get('/api/direct/browse', {
params: {
a: 'filtered',
page: this.filteredPage
}
})
.then(res => {
self.loaded = true;
this.threads = res.data
this.messages.filtered = res.data;
});
}
}
}
}
</script>

View File

@ -0,0 +1,682 @@
<template>
<div>
<div v-if="loaded && page == 'read'" class="container messages-page p-0 p-md-2 mt-n4" style="min-height: 60vh;">
<div class="col-12 col-md-8 offset-md-2 p-0 px-md-2">
<div class="card shadow-none border mt-4">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<span>
<a href="/account/direct" class="text-muted">
<i class="fas fa-chevron-left fa-lg"></i>
</a>
</span>
<span>
<div class="media">
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" alt="Generic placeholder image" width="40px">
<div class="media-body">
<p class="mb-0">
<span class="font-weight-bold">{{thread.name}}</span>
</p>
<p class="mb-0">
<a v-if="!thread.isLocal" :href="'/'+thread.username" class="text-decoration-none text-muted">{{thread.username}}</a>
<a v-else :href="'/'+thread.username" class="text-decoration-none text-muted">&commat;{{thread.username}}</a>
</p>
</div>
</div>
</span>
<span><a href="#" class="text-muted" @click.prevent="showOptions()"><i class="fas fa-cog fa-lg"></i></a></span>
</div>
<ul class="list-group list-group-flush dm-wrapper" style="height:60vh;overflow-y: scroll;">
<li class="list-group-item border-0">
<p class="text-center small text-muted">
Conversation with <span class="font-weight-bold">{{thread.username}}</span>
</p>
<hr>
</li>
<li v-if="showLoadMore && thread.messages && thread.messages.length > 5" class="list-group-item border-0 mt-n4">
<p class="text-center small text-muted">
<button v-if="!loadingMessages" class="btn btn-primary font-weight-bold rounded-pill btn-sm px-3" @click="loadOlderMessages()">Load Older Messages</button>
<button v-else class="btn btn-primary font-weight-bold rounded-pill btn-sm px-3" disabled>Loading...</button>
</p>
</li>
<li v-for="(convo, index) in thread.messages" class="list-group-item border-0 chat-msg cursor-pointer" @click="openCtxMenu(convo, index)">
<div v-if="!convo.isAuthor" class="media d-inline-flex mb-0">
<img v-if="!hideAvatars" class="mr-3 mt-2 rounded-circle img-thumbnail" :src="thread.avatar" alt="avatar" width="32px">
<div class="media-body">
<p v-if="convo.type == 'photo'" class="pill-to p-0 shadow">
<img :src="convo.media" width="140px" style="border-radius:20px;">
</p>
<div v-else-if="convo.type == 'link'" class="media d-inline-flex mb-0 cursor-pointer">
<div class="media-body">
<div class="card mb-2 rounded border shadow" style="width:240px;" :title="convo.text">
<div class="card-body p-0">
<div class="media d-flex align-items-center">
<div v-if="convo.meta.local" class="bg-primary mr-3 border-right p-3">
<i class="fas fa-link text-white fa-2x"></i>
</div>
<div v-else class="bg-light mr-3 border-right p-3">
<i class="fas fa-link text-lighter fa-2x"></i>
</div>
<div class="media-body text-muted small text-truncate pr-2 font-weight-bold">
{{convo.meta.local ? convo.text.substr(8) : convo.meta.domain}}
</div>
</div>
</div>
</div>
</div>
</div>
<p v-else-if="convo.type == 'video'" class="pill-to p-0 shadow">
<!-- <video :src="convo.media" width="140px" style="border-radius:20px;"></video> -->
<span class="d-block bg-primary d-flex align-items-center justify-content-center" style="width:200px;height: 110px;border-radius: 20px;">
<div class="text-center">
<p class="mb-1">
<i class="fas fa-play fa-2x text-white"></i>
</p>
<p class="mb-0 small font-weight-bold text-white">
Play
</p>
</div>
</span>
</p>
<p v-else-if="convo.type == 'emoji'" class="p-0 emoji-msg">
{{convo.text}}
</p>
<p v-else :class="[largerText ? 'pill-to shadow larger-text text-break':'pill-to shadow text-break']">
{{convo.text}}
</p>
<p v-if="!hideTimestamps" class="small text-muted font-weight-bold ml-2 d-flex align-items-center justify-content-start" data-timestamp="timestamp"> <span v-if="convo.hidden" class="mr-2 small" title="Filtered Message" data-toggle="tooltip" data-placement="bottom"><i class="fas fa-lock"></i></span> {{convo.timeAgo}}</p>
</div>
</div>
<div v-else class="media d-inline-flex float-right mb-0">
<div class="media-body">
<p v-if="convo.type == 'photo'" class="pill-from p-0 shadow">
<img :src="convo.media" width="140px" style="border-radius:20px;">
</p>
<div v-else-if="convo.type == 'link'" class="media d-inline-flex float-right mb-0 cursor-pointer">
<div class="media-body">
<div class="card mb-2 rounded border shadow" style="width:240px;" :title="convo.text">
<div class="card-body p-0">
<div class="media d-flex align-items-center">
<div v-if="convo.meta.local" class="bg-primary mr-3 border-right p-3">
<i class="fas fa-link text-white fa-2x"></i>
</div>
<div v-else class="bg-light mr-3 border-right p-3">
<i class="fas fa-link text-lighter fa-2x"></i>
</div>
<div class="media-body text-muted small text-truncate pr-2 font-weight-bold">
{{convo.meta.local ? convo.text.substr(8) : convo.meta.domain}}
</div>
</div>
</div>
</div>
</div>
</div>
<p v-else-if="convo.type == 'video'" class="pill-from p-0 shadow">
<!-- <video :src="convo.media" width="140px" style="border-radius:20px;"></video> -->
<span class="rounded-pill bg-primary d-flex align-items-center justify-content-center" style="width:200px;height: 110px">
<div class="text-center">
<p class="mb-1">
<i class="fas fa-play fa-2x text-white"></i>
</p>
<p class="mb-0 small font-weight-bold">
Play
</p>
</div>
</span>
</p>
<p v-else-if="convo.type == 'emoji'" class="p-0 emoji-msg">
{{convo.text}}
</p>
<p v-else :class="[largerText ? 'pill-from shadow larger-text text-break':'pill-from shadow text-break']">
{{convo.text}}
</p>
<p v-if="!hideTimestamps" class="small text-muted font-weight-bold text-right mr-2"> <span v-if="convo.hidden" class="mr-2 small" title="Filtered Message" data-toggle="tooltip" data-placement="bottom"><i class="fas fa-lock"></i></span> {{convo.timeAgo}}
</p>
</div>
<img v-if="!hideAvatars" class="ml-3 mt-2 rounded-circle img-thumbnail" :src="profile.avatar" alt="avatar" width="32px">
</div>
</li>
</ul>
<div class="card-footer bg-white p-0">
<form class="border-0 rounded-0 align-middle" method="post" action="#">
<textarea class="form-control border-0 rounded-0 no-focus" name="comment" placeholder="Reply ..." autocomplete="off" autocorrect="off" style="height:86px;line-height: 18px;max-height:80px;resize: none; padding-right:115.22px;" v-model="replyText" :disabled="blocked"></textarea>
<input type="button" value="Send" :class="[replyText.length ? 'd-inline-block btn btn-sm btn-primary rounded-pill font-weight-bold reply-btn text-decoration-none text-uppercase' : 'd-inline-block btn btn-sm btn-primary rounded-pill font-weight-bold reply-btn text-decoration-none text-uppercase disabled']" :disabled="replyText.length == 0" @click.prevent="sendMessage"/>
</form>
</div>
<div class="card-footer p-0">
<p class="d-flex justify-content-between align-items-center mb-0 px-3 py-1 small">
<!-- <span class="font-weight-bold" style="color: #D69E2E">
<i class="fas fa-circle mr-1"></i>
Typing ...
</span> -->
<span>
<!-- <span class="btn btn-primary btn-sm font-weight-bold py-0 px-3 rounded-pill" @click="uploadMedia">
<i class="fas fa-share mr-1"></i>
Share
</span> -->
<span class="btn btn-primary btn-sm font-weight-bold py-0 px-3 rounded-pill" @click="uploadMedia">
<i class="fas fa-upload mr-1"></i>
Add Photo/Video
</span>
</span>
<input type="file" id="uploadMedia" class="d-none" name="uploadMedia" accept="image/jpeg,image/png,image/gif,video/mp4" >
<span class="text-muted font-weight-bold">{{replyText.length}}/600</span>
</p>
</div>
</div>
</div>
</div>
<div v-if="loaded && page == 'options'" class="container messages-page p-0 p-md-2 mt-n4" style="min-height: 60vh;">
<div class="col-12 col-md-8 offset-md-2 p-0 px-md-2">
<div class="card shadow-none border mt-4">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<span>
<a href="#" class="text-muted" @click.prevent="page='read'">
<i class="fas fa-chevron-left fa-lg"></i>
</a>
</span>
<span>
<p class="mb-0 lead font-weight-bold py-2">Message Settings</p>
</span>
<span class="text-lighter" data-toggle="tooltip" data-placement="bottom" title="Have a nice day!"><i class="far fa-smile fa-lg"></i></span>
</div>
<ul class="list-group list-group-flush dm-wrapper" style="height: 698px;">
<div class="list-group-item media border-bottom">
<div class="d-inline-block custom-control custom-switch ml-3">
<input type="checkbox" class="custom-control-input" id="customSwitch0" v-model="hideAvatars">
<label class="custom-control-label" for="customSwitch0"></label>
</div>
<div class="d-inline-block ml-3 font-weight-bold">
Hide Avatars
</div>
</div>
<div class="list-group-item media border-bottom">
<div class="d-inline-block custom-control custom-switch ml-3">
<input type="checkbox" class="custom-control-input" id="customSwitch1" v-model="hideTimestamps">
<label class="custom-control-label" for="customSwitch1"></label>
</div>
<div class="d-inline-block ml-3 font-weight-bold">
Hide Timestamps
</div>
</div>
<div class="list-group-item media border-bottom">
<div class="d-inline-block custom-control custom-switch ml-3">
<input type="checkbox" class="custom-control-input" id="customSwitch2" v-model="largerText">
<label class="custom-control-label" for="customSwitch2"></label>
</div>
<div class="d-inline-block ml-3 font-weight-bold">
Larger Text
</div>
</div>
<!-- <div class="list-group-item media border-bottom">
<div class="d-inline-block custom-control custom-switch ml-3">
<input type="checkbox" class="custom-control-input" id="customSwitch3" v-model="autoRefresh">
<label class="custom-control-label" for="customSwitch3"></label>
</div>
<div class="d-inline-block ml-3 font-weight-bold">
Auto Refresh
</div>
</div> -->
<div class="list-group-item media border-bottom d-flex align-items-center">
<div class="d-inline-block custom-control custom-switch ml-3">
<input type="checkbox" class="custom-control-input" id="customSwitch4" v-model="mutedNotifications">
<label class="custom-control-label" for="customSwitch4"></label>
</div>
<div class="d-inline-block ml-3 font-weight-bold">
Mute Notifications
<p class="small mb-0">You will not receive any direct message notifications from <strong>{{thread.username}}</strong>.</p>
</div>
</div>
</ul>
</div>
</div>
</div>
<b-modal ref="ctxModal"
id="ctx-modal"
hide-header
hide-footer
centered
rounded
size="sm"
body-class="list-group-flush p-0 rounded">
<div class="list-group text-center">
<div v-if="ctxContext && ctxContext.type == 'photo'" class="list-group-item rounded cursor-pointer font-weight-bold text-dark" @click="viewOriginal()">View Original</div>
<div v-if="ctxContext && ctxContext.type == 'video'" class="list-group-item rounded cursor-pointer font-weight-bold text-dark" @click="viewOriginal()">Play</div>
<div v-if="ctxContext && ctxContext.type == 'link'" class="list-group-item rounded cursor-pointer" @click="clickLink()">
<p class="mb-0" style="font-size:12px;">
Navigate to
</p>
<p class="mb-0 font-weight-bold text-dark">
{{this.ctxContext.meta.domain}}
</p>
</div>
<div v-if="ctxContext && (ctxContext.type == 'text' || ctxContext.type == 'emoji' || ctxContext.type == 'link')" class="list-group-item rounded cursor-pointer text-dark" @click="copyText()">Copy</div>
<div v-if="ctxContext && !ctxContext.isAuthor" class="list-group-item rounded cursor-pointer text-muted" @click="reportMessage()">Report</div>
<div v-if="ctxContext && ctxContext.isAuthor" class="list-group-item rounded cursor-pointer text-muted" @click="deleteMessage()">Delete</div>
<div class="list-group-item rounded cursor-pointer text-lighter" @click="closeCtxMenu()">Cancel</div>
</div>
</b-modal>
</div>
</template>
<style type="text/css" scoped>
.reply-btn {
position: absolute;
bottom: 54px;
right: 20px;
width: 90px;
text-align: center;
border-radius: 0 3px 3px 0;
}
.media-body .bg-primary {
background: linear-gradient(135deg, #2EA2F4 0%, #0B93F6 100%) !important;
}
.pill-to {
background:#EDF2F7;
font-weight: 500;
border-radius: 20px !important;
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
margin-right: 3rem;
margin-bottom: 0.25rem;
}
.pill-from {
color: white !important;
text-align: right !important;
/*background: #53d769;*/
background: linear-gradient(135deg, #2EA2F4 0%, #0B93F6 100%) !important;
font-weight: 500;
border-radius: 20px !important;
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
margin-left: 3rem;
margin-bottom: 0.25rem;
}
.chat-msg:hover {
background: #f7fbfd;
}
.no-focus:focus {
outline: none !important;
outline-width: 0 !important;
box-shadow: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
}
.emoji-msg {
font-size: 4rem !important;
line-height: 30px !important;
margin-top: 10px !important;
}
.larger-text {
font-size: 22px;
}
</style>
<script type="text/javascript">
export default {
props: ['accountId'],
data() {
return {
config: window.App.config,
hideAvatars: true,
hideTimestamps: false,
largerText: false,
autoRefresh: false,
mutedNotifications: false,
blocked: false,
loaded: false,
profile: {},
page: 'read',
pages: ['browse', 'add', 'read'],
threads: [],
thread: false,
threadIndex: false,
replyText: '',
composeUsername: '',
ctxContext: null,
ctxIndex: null,
uploading: false,
uploadProgress: null,
min_id: null,
max_id: null,
loadingMessages: false,
showLoadMore: true,
}
},
mounted() {
this.fetchProfile();
let self = this;
axios.get('/api/direct/thread', {
params: {
pid: self.accountId
}
})
.then(res => {
self.loaded = true;
let d = res.data;
d.messages.reverse();
this.thread = d;
this.threads = [d];
this.threadIndex = 0;
let mids = d.messages.map(m => m.id);
this.max_id = Math.max(...mids);
this.min_id = Math.min(...mids);
this.mutedNotifications = d.muted;
this.markAsRead();
//this.messagePoll();
setTimeout(function() {
let objDiv = document.querySelector('.dm-wrapper');
objDiv.scrollTop = objDiv.scrollHeight;
}, 300);
});
let options = localStorage.getItem('px_dm_options');
if(options) {
options = JSON.parse(options);
this.hideAvatars = options.hideAvatars;
this.hideTimestamps = options.hideTimestamps;
this.largerText = options.largerText;
}
},
watch: {
mutedNotifications: function(v) {
if(v) {
axios.post('/api/direct/mute', {
id: this.accountId
}).then(res => {
});
} else {
axios.post('/api/direct/unmute', {
id: this.accountId
}).then(res => {
});
}
this.mutedNotifications = v;
},
hideAvatars: function(v) {
this.hideAvatars = v;
this.updateOptions();
},
hideTimestamps: function(v) {
this.hideTimestamps = v;
this.updateOptions();
},
largerText: function(v) {
this.largerText = v;
this.updateOptions();
},
},
updated() {
$('[data-toggle="tooltip"]').tooltip();
},
methods: {
fetchProfile() {
axios.get('/api/pixelfed/v1/accounts/verify_credentials').then(res => {
this.profile = res.data;
window._sharedData.curUser = res.data;
});
},
sendMessage() {
let self = this;
let rt = this.replyText;
axios.post('/api/direct/create', {
'to_id': this.threads[this.threadIndex].id,
'message': rt,
'type': self.isEmoji(rt) && rt.length < 10 ? 'emoji' : 'text'
}).then(res => {
let msg = res.data;
self.threads[self.threadIndex].messages.push(msg);
let mids = self.threads[self.threadIndex].messages.map(m => m.id);
this.max_id = Math.max(...mids)
this.min_id = Math.min(...mids)
setTimeout(function() {
var objDiv = document.querySelector('.dm-wrapper');
objDiv.scrollTop = objDiv.scrollHeight;
}, 300);
}).catch(err => {
if(err.response.status == 403) {
self.blocked = true;
swal('Profile Unavailable', 'You cannot message this profile at this time.', 'error');
}
})
this.replyText = '';
},
openCtxMenu(r, i) {
this.ctxIndex = i;
this.ctxContext = r;
this.$refs.ctxModal.show();
},
closeCtxMenu() {
this.$refs.ctxModal.hide();
},
truncate(t) {
return _.truncate(t);
},
deleteMessage() {
let self = this;
let c = window.confirm('Are you sure you want to delete this message?');
if(c) {
axios.delete('/api/direct/message', {
params: {
id: self.ctxContext.reportId
}
}).then(res => {
self.threads[self.threadIndex].messages.splice(self.ctxIndex,1);
self.closeCtxMenu();
});
} else {
self.closeCtxMenu();
}
},
reportMessage() {
this.closeCtxMenu();
let url = '/i/report?type=post&id=' + this.ctxContext.reportId;
window.location.href = url;
return;
},
uploadMedia(event) {
let self = this;
$(document).on('change', '#uploadMedia', function(e) {
self.handleUpload();
});
let el = $(event.target);
el.attr('disabled', '');
$('#uploadMedia').click();
el.blur();
el.removeAttr('disabled');
},
handleUpload() {
let self = this;
self.uploading = true;
let io = document.querySelector('#uploadMedia');
if(!io.files.length) {
this.uploading = false;
}
Array.prototype.forEach.call(io.files, function(io, i) {
let type = io.type;
let acceptedMimes = self.config.uploader.media_types.split(',');
let validated = $.inArray(type, acceptedMimes);
if(validated == -1) {
swal('Invalid File Type', 'The file you are trying to add is not a valid mime type. Please upload a '+self.config.uploader.media_types+' only.', 'error');
self.uploading = false;
return;
}
let form = new FormData();
form.append('file', io);
form.append('to_id', self.threads[self.threadIndex].id);
let xhrConfig = {
onUploadProgress: function(e) {
let progress = Math.round( (e.loaded * 100) / e.total );
self.uploadProgress = progress;
}
};
axios.post('/api/direct/media', form, xhrConfig)
.then(function(e) {
self.uploadProgress = 100;
self.uploading = false;
let msg = {
id: e.data.id,
type: e.data.type,
reportId: e.data.reportId,
isAuthor: true,
text: null,
media: e.data.url,
timeAgo: '1s',
seen: null
};
self.threads[self.threadIndex].messages.push(msg);
setTimeout(function() {
var objDiv = document.querySelector('.dm-wrapper');
objDiv.scrollTop = objDiv.scrollHeight;
}, 300);
}).catch(function(e) {
switch(e.response.status) {
case 451:
self.uploading = false;
io.value = null;
swal('Banned Content', 'This content has been banned and cannot be uploaded.', 'error');
break;
default:
self.uploading = false;
io.value = null;
swal('Oops, something went wrong!', 'An unexpected error occurred.', 'error');
break;
}
});
io.value = null;
self.uploadProgress = 0;
});
},
viewOriginal() {
let url = this.ctxContext.media;
window.location.href = url;
return;
},
isEmoji(text) {
const onlyEmojis = text.replace(new RegExp('[\u0000-\u1eeff]', 'g'), '')
const visibleChars = text.replace(new RegExp('[\n\r\s]+|( )+', 'g'), '')
return onlyEmojis.length === visibleChars.length
},
copyText() {
window.App.util.clipboard(this.ctxContext.text);
this.closeCtxMenu();
return;
},
clickLink() {
let url = this.ctxContext.text;
if(this.ctxContext.meta.local != true) {
url = '/i/redirect?url=' + encodeURI(this.ctxContext.text);
}
window.location.href = url;
},
markAsRead() {
return;
axios.post('/api/direct/read', {
pid: this.accountId,
sid: this.max_id
}).then(res => {
}).catch(err => {
});
},
loadOlderMessages() {
let self = this;
this.loadingMessages = true;
axios.get('/api/direct/thread', {
params: {
pid: this.accountId,
max_id: this.min_id,
}
}).then(res => {
let d = res.data;
if(!d.messages.length) {
this.showLoadMore = false;
this.loadingMessages = false;
return;
}
let cids = this.thread.messages.map(m => m.id);
let m = d.messages.filter(m => {
return cids.indexOf(m.id) == -1;
}).reverse();
let mids = m.map(m => m.id);
let min_id = Math.min(...mids);
if(min_id == this.min_id) {
this.showLoadMore = false;
this.loadingMessages = false;
return;
}
this.min_id = min_id;
this.thread.messages.unshift(...m);
setTimeout(function() {
self.loadingMessages = false;
}, 500);
}).catch(err => {
this.loadingMessages = false;
})
},
messagePoll() {
let self = this;
setInterval(function() {
axios.get('/api/direct/thread', {
params: {
pid: self.accountId,
min_id: self.thread.messages[self.thread.messages.length - 1].id
}
}).then(res => {
});
}, 5000);
},
showOptions() {
this.page = 'options';
},
updateOptions() {
let options = {
v: 1,
hideAvatars: this.hideAvatars,
hideTimestamps: this.hideTimestamps,
largerText: this.largerText
}
window.localStorage.setItem('px_dm_options', JSON.stringify(options));
}
}
}
</script>

View File

@ -62,6 +62,11 @@
<a :href="n.account.url" class="font-weight-bold text-dark word-break" :title="n.account.username">{{truncate(n.account.username)}}</a> tagged you in a <a class="font-weight-bold" v-bind:href="n.tagged.post_url">post</a>.
</p>
</div>
<div v-else-if="n.type == 'direct'">
<p class="my-0">
<a :href="n.account.url" class="font-weight-bold text-dark word-break" :title="n.account.username">{{truncate(n.account.username)}}</a> sent a <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">dm</a>.
</p>
</div>
<div v-else>
<p class="my-0">
We cannot display this notification at this time.

View File

@ -102,17 +102,18 @@
</span>
<span v-if="profile.id != user.id && user.hasOwnProperty('id')">
<span class="pl-4" v-if="relationship.following == true">
<button type="button" class="btn btn-outline-secondary font-weight-bold btn-sm py-1" v-on:click="followProfile" data-toggle="tooltip" title="Unfollow">FOLLOWING</button>
<a :href="'/account/direct/t/'+profile.id" class="btn btn-outline-secondary font-weight-bold btn-sm py-1 text-dark mr-2 px-3 btn-sec-alt" style="border:1px solid #dbdbdb;" data-toggle="tooltip" title="Message">Message</a>
<button type="button" class="btn btn-outline-secondary font-weight-bold btn-sm py-1 text-dark btn-sec-alt" style="border:1px solid #dbdbdb;" v-on:click="followProfile" data-toggle="tooltip" title="Unfollow"><i class="fas fa-user-check mx-3"></i></button>
</span>
<span class="pl-4" v-if="!relationship.following">
<button type="button" class="btn btn-primary font-weight-bold btn-sm py-1" v-on:click="followProfile" data-toggle="tooltip" title="Follow">FOLLOW</button>
<button type="button" class="btn btn-primary font-weight-bold btn-sm py-1 px-3" v-on:click="followProfile" data-toggle="tooltip" title="Follow">Follow</button>
</span>
</span>
<span class="pl-4" v-if="owner && user.hasOwnProperty('id')">
<a class="btn btn-outline-secondary btn-sm" href="/settings/home" style="font-weight: 600;">Edit Profile</a>
</span>
<span class="pl-4">
<a class="fas fa-ellipsis-h fa-lg text-muted text-decoration-none" href="#" @click.prevent="visitorMenu"></a>
<a class="fas fa-ellipsis-h fa-lg text-dark text-decoration-none" href="#" @click.prevent="visitorMenu"></a>
</span>
</div>
<div class="font-size-16px">
@ -614,6 +615,12 @@
.modal-tab-active {
border-bottom: 1px solid #08d;
}
.btn-sec-alt:hover {
color: #ccc;
opacity: .7;
background-color: transparent;
border-color: #6c757d;
}
</style>
<script type="text/javascript">
import VueMasonry from 'vue-masonry-css'

View File

@ -25,7 +25,12 @@
<button v-if="relationship && relationship.following == false" class="btn btn-outline-light py-0 px-3 mt-n1" style="font-size:13px; font-weight: 500;" @click="followProfile();">Follow</button>
<button v-if="relationship && relationship.following == true" class="btn btn-outline-light py-0 px-3 mt-n1" style="font-size:13px; font-weight: 500;" @click="unfollowProfile();">Unfollow</button>
</span>
<span class="ml-3">
<span class="mx-2">
<a :href="'/account/direct/t/' + profile.id" class="btn btn-outline-light btn-sm mt-n1" style="padding-top:2px;padding-bottom:1px;">
<i class="far fa-comment-dots cursor-pointer" style="font-size:13px;"></i>
</a>
</span>
<span>
<button class="btn btn-outline-light btn-sm mt-n1" @click="showCtxMenu()" style="padding-top:2px;padding-bottom:1px;">
<i class="fas fa-cog cursor-pointer" style="font-size:13px;"></i>
</button>
@ -114,8 +119,7 @@
<div v-if="feed.length == 0" class="col-12 mb-2">
<div class="d-flex justify-content-center align-items-center bg-white border rounded" style="height:60vh;">
<div class="text-center">
<p class="mb-0 lead">No posts found.</p>
<p class="">We haven't seen any posts from this account.</p>
<p class="lead">We haven't seen any posts from this account.</p>
</div>
</div>
</div>

View File

@ -24,7 +24,7 @@
<div class="col-12 mb-5">
<hr>
</div>
<div v-if="placesSearchEnabled && showPlaces" class="col-12">
<div v-if="placesSearchEnabled && showPlaces" class="col-12 mb-4">
<div class="mb-4">
<p class="text-secondary small font-weight-bold">PLACES <span class="pl-1 text-lighter">({{results.placesPagination.total}})</span></p>
</div>

View File

@ -1,4 +1,9 @@
Vue.component(
'direct-component',
require('./components/Direct.vue').default
);
Vue.component(
'direct-message',
require('./components/DirectMessage.vue').default
);

View File

@ -7,11 +7,6 @@
@endsection
@push('scripts')
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/direct.js') }}"></script>
<script type="text/javascript">
new Vue({
el: '#content'
});
</script>
<script type="text/javascript">App.boot();</script>
@endpush

View File

@ -0,0 +1,12 @@
@extends('layouts.app')
@section('content')
<div>
<direct-message account-id="{{$id}}"></direct-message>
</div>
@endsection
@push('scripts')
<script type="text/javascript" src="{{ mix('js/direct.js') }}"></script>
<script type="text/javascript">App.boot();</script>
@endpush

View File

@ -38,6 +38,12 @@
<span class="sr-only">Discover</span>
</a>
</li>
<li class="nav-item px-md-2">
<a class="nav-link font-weight-bold text-muted" href="/account/direct" title="Direct" data-toggle="tooltip" data-placement="bottom">
<i class="far fa-comment-dots fa-lg"></i>
<span class="sr-only">Direct</span>
</a>
</li>
<li class="nav-item px-md-2 d-none d-md-block">
<a class="nav-link font-weight-bold text-muted" href="/account/activity" title="Notifications" data-toggle="tooltip" data-placement="bottom">
<i class="far fa-bell fa-lg"></i>

View File

@ -40,13 +40,13 @@
</label>
<p class="text-muted small help-text">When this option is enabled, your profile and posts are used for discover recommendations. Only public profiles and posts are used.</p>
</div> --}}
{{--<div class="form-check pb-3">
<input class="form-check-input" type="checkbox" value="" id="dm">
<label class="form-check-label font-weight-bold" for="dm">
<div class="form-check pb-3">
<input class="form-check-input" type="checkbox" id="public_dm" {{$settings->public_dm ? 'checked=""':''}} name="public_dm">
<label class="form-check-label font-weight-bold" for="public_dm">
{{__('Receive Direct Messages from anyone')}}
</label>
<p class="text-muted small help-text">If selected, you will be able to receive messages from any user even if you do not follow them.</p>
</div>--}}
<p class="text-muted small help-text">If selected, you will be able to receive messages and notifications from any user even if you do not follow them.</p>
</div>
{{-- <div class="form-check pb-3">
<input class="form-check-input" type="checkbox" value="" id="srs" checked="">
<label class="form-check-label font-weight-bold" for="srs">

View File

@ -100,22 +100,22 @@
</div>
</a>
</div>
{{-- <div class="col-12 col-md-6 mb-3">
<div class="col-12 col-md-6 mb-3">
<a href="{{route('help.dm')}}" class="text-decoration-none">
<div class="card">
<div class="card-body">
<p class="py-1 text-center">
<i class="far fa-envelope text-lighter fa-2x"></i>
<i class="far fa-comment-dots text-lighter fa-2x"></i>
</p>
<p class="text-center text-muted font-weight-bold h4 mb-0">{{__('helpcenter.directMessages')}}</p>
<div class="text-center pt-3">
<p class="small text-dark font-weight-bold mb-0">&nbsp;</p>
<p class="small text-dark font-weight-bold mb-0">&nbsp;</p>
<p class="small text-dark font-weight-bold mb-0">How do I use Pixelfed Direct?</p>
<p class="small text-dark font-weight-bold mb-0">How do I unsend a message?</p>
</div>
</div>
</div>
</a>
</div> --}}
</div>
{{-- <div class="col-12 col-md-6 mb-3">
<a href="{{route('help.stories')}}" class="text-decoration-none">
<div class="card">

View File

@ -2,25 +2,88 @@
@section('section')
<div class="title">
<h3 class="font-weight-bold">Direct Messages</h3>
<div class="title">
<h3 class="font-weight-bold">{{__('helpcenter.directMessages')}}</h3>
</div>
<hr>
<p class="lead ">Send and recieve direct messages from other profiles.</p>
<hr>
<p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse1" role="button" aria-expanded="false" aria-controls="collapse1">
<i class="fas fa-chevron-down mr-2"></i>
How do I use Pixelfed Direct?
</a>
<div class="collapse" id="collapse1">
<div>
<p>Pixelfed Direct lets you send messages to another account. You can send the following things as a message on Pixelfed Direct:</p>
<ul>
<li>
Photos or videos you take or upload from your library
</li>
<li>
Posts you see in feed
</li>
<li>
Profiles
</li>
<li>
Text
</li>
<li>
Hashtags
</li>
<li>
Locations
</li>
</ul>
<p>To see messages you've sent with Pixelfed Direct, tap <i class="far fa-comment-dots"></i> in the top right of feed. From there, you can manage the messages you've sent and received.</p>
<p>Photos or videos sent with Pixelfed Direct can't be shared through Pixelfed to other sites like Mastodon or Twitter, and won't appear on hashtag and location pages.</p>
</div>
</div>
<hr>
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-12 col-md-3 text-center">
<div class="icon-wrapper">
<i class="far fa-question-circle fa-3x text-light"></i>
</div>
</div>
<div class="col-12 col-md-9 d-flex align-items-center">
<div class="text-center">
<p class="h3 font-weight-bold mb-0">This page isn't available</p>
<p class="font-weight-light mb-0">We haven't finished it yet, it will be updated soon!</p>
</div>
</div>
</div>
</div>
</p>
{{-- <p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse2" role="button" aria-expanded="false" aria-controls="collapse2">
<i class="fas fa-chevron-down mr-2"></i>
How do I manage messages I've recieved with Pixelfed Direct?
</a>
<div class="collapse" id="collapse2">
<div>
</div>
</div>
</p> --}}
<p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse3" role="button" aria-expanded="false" aria-controls="collapse3">
<i class="fas fa-chevron-down mr-2"></i>
How do I unsend a message I've sent using Pixelfed Direct?
</a>
<div class="collapse" id="collapse3">
<div class="mt-2">
You can click the message and select the <strong>Delete</strong> option.
</div>
</div>
</p>
<p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse4" role="button" aria-expanded="false" aria-controls="collapse4">
<i class="fas fa-chevron-down mr-2"></i>
Can I use Pixelfed Direct to send messages to people Im not following?
</a>
<div class="collapse" id="collapse4">
<div class="mt-2">
You can send a message to someone you are not following though it may be sent to their filtered inbox and not easily seen.
</div>
</div>
</p>
<p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse5" role="button" aria-expanded="false" aria-controls="collapse5">
<i class="fas fa-chevron-down mr-2"></i>
How do I report content that I've recieved in a Pixelfed Direct message?
</a>
<div class="collapse" id="collapse5">
<div class="mt-2">
You can click the message and then select the <strong>Report</strong> option and follow the instructions on the Report page.
</div>
</div>
</p>
@endsection

View File

@ -97,6 +97,18 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('search', 'SearchController@searchAPI');
Route::get('nodeinfo/2.0.json', 'FederationController@nodeinfo');
Route::group(['prefix' => 'direct'], function () {
Route::get('browse', 'DirectMessageController@browse');
Route::post('create', 'DirectMessageController@create');
Route::get('thread', 'DirectMessageController@thread');
Route::post('mute', 'DirectMessageController@mute');
Route::post('unmute', 'DirectMessageController@unmute');
Route::delete('message', 'DirectMessageController@delete');
Route::post('media', 'DirectMessageController@mediaUpload');
Route::post('lookup', 'DirectMessageController@composeLookup');
Route::post('read', 'DirectMessageController@read');
});
Route::group(['prefix' => 'v2'], function() {
Route::get('config', 'ApiController@siteConfiguration');
Route::get('discover', 'InternalApiController@discover');
@ -287,6 +299,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::group(['prefix' => 'account'], function () {
Route::redirect('/', '/');
Route::get('direct', 'AccountController@direct');
Route::get('direct/t/{id}', 'AccountController@directMessage');
Route::get('activity', 'AccountController@notifications')->name('notifications');
Route::get('follow-requests', 'AccountController@followRequests')->name('follow-requests');
Route::post('follow-requests', 'AccountController@followRequestHandle');

2
webpack.mix.js vendored
View File

@ -35,7 +35,7 @@ mix.js('resources/assets/js/app.js', 'public/js')
.js('resources/assets/js/profile-directory.js', 'public/js')
.js('resources/assets/js/story-compose.js', 'public/js')
// .js('resources/assets/js/embed.js', 'public')
// .js('resources/assets/js/direct.js', 'public/js')
.js('resources/assets/js/direct.js', 'public/js')
// .js('resources/assets/js/admin.js', 'public/js')
// .js('resources/assets/js/micro.js', 'public/js')
.js('resources/assets/js/rempro.js', 'public/js')