feat: change avatar management (#2112)

This commit is contained in:
Regis Freyd
2019-08-17 15:32:10 -04:00
committed by Alexis Saettler
parent 25fa60911a
commit 72ff0d3da4
89 changed files with 2749 additions and 719 deletions
+1
View File
@@ -2,6 +2,7 @@
### New features:
* Add ability to change the avatar of your contacts
* Add the ablity to set a 'me' contact (only API for now)
* Add stepparent/stepchild relationship
+4 -1
View File
@@ -14,6 +14,7 @@ use App\Models\Contact\ContactField;
use App\Models\Contact\ContactFieldType;
use App\Services\Contact\Address\CreateAddress;
use App\Services\Contact\Reminder\CreateReminder;
use App\Services\Contact\Avatar\GetAvatarsFromInternet;
class ImportCSV extends Command
{
@@ -214,7 +215,9 @@ class ImportCSV extends Command
]);
}
$contact->updateGravatar();
app(GetAvatarsFromInternet::class)->execute([
'contact_id' => $contact->id,
]);
}
/**
@@ -0,0 +1,87 @@
<?php
namespace App\Console\Commands\OneTime;
use App\Events\MoveAvatarEvent;
use App\Models\Contact\Contact;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Event;
use Illuminate\Console\ConfirmableTrait;
use App\Exceptions\FileNotFoundException;
use Symfony\Component\Console\Output\OutputInterface;
use App\Jobs\Avatars\MoveContactAvatarToPhotosDirectory;
/**
* This command moves current avatars to the new Photos directory and converts
* each avatar to a Photo object.
*/
class MoveAvatarsToPhotosDirectory extends Command
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:moveavatarstophotosdirectory
{--force : Force the operation to run when in production.}
{--dryrun : Simulate the execution but not write anything.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Move avatars to the Photos directory, and create a photo object for each one of them';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (! $this->confirmToProceed()) {
return;
}
Event::listen(MoveAvatarEvent::class, function ($event) {
$this->handleEvent($event->contact);
});
$delay = now();
Contact::where('has_avatar', true)
->chunk(100, function ($contacts) use ($delay) {
foreach ($contacts as $contact) {
if ($contact->avatar_source === 'default') {
$this->handleContact($contact, $delay);
}
}
// add some delay, so we treat 100 contacts each minutes
$delay = $delay->addMinutes(1);
});
}
private function handleContact($contact, $delay)
{
try {
if ($this->option('dryrun')) {
MoveContactAvatarToPhotosDirectory::dispatchNow($contact, true);
} else {
MoveContactAvatarToPhotosDirectory::dispatch($contact, false)
->delay($delay);
}
} catch (FileNotFoundException $e) {
if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
$this->warn(' ! File not found: '.$e->fileName);
}
}
}
private function handleEvent($contact)
{
$this->info('Contact id:'.$contact->id.' | Avatar location:'.$contact->avatar_location.' | File name:'.$contact->avatar_file_name);
}
}
+2
View File
@@ -25,6 +25,7 @@ use App\Console\Commands\CalculateStatistics;
use App\Console\Commands\OneTime\MoveAvatars;
use App\Console\Commands\MigrateDatabaseCollation;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\Console\Commands\OneTime\MoveAvatarsToPhotosDirectory;
class Kernel extends ConsoleKernel
{
@@ -44,6 +45,7 @@ class Kernel extends ConsoleKernel
LangGenerate::class,
MigrateDatabaseCollation::class,
MoveAvatars::class,
MoveAvatarsToPhotosDirectory::class,
PingVersionServer::class,
SendReminders::class,
SendStayInTouch::class,
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Events;
use App\Models\Contact\Contact;
use Illuminate\Queue\SerializesModels;
class MoveAvatarEvent extends Event
{
use SerializesModels;
/**
* The contact.
*
* @var Contact
*/
public $contact;
/**
* Create a new event instance.
*
* @param Contact $contact
* @return void
*/
public function __construct($contact)
{
$this->contact = $contact;
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Exceptions;
use Illuminate\Contracts\Filesystem\FileNotFoundException as FileNotFoundExceptionBase;
class FileNotFoundException extends FileNotFoundExceptionBase
{
/**
* @var string
*/
public $fileName;
/**
* Create a new instance.
*
* @param string $fileName
* @return void
*/
public function __construct($fileName)
{
$this->fileName = $fileName;
parent::__construct();
}
public function __toString()
{
return 'File not found: '.$this->fileName;
}
}
-33
View File
@@ -1,33 +0,0 @@
<?php
namespace App\Helpers;
use App\Models\Contact\Contact;
class AvatarHelper
{
/**
* Get an avatar.
*
* @return string
*/
public static function get(Contact $contact, $size)
{
$htmlString = '<div class="absolute profile-page-avatar">';
if ($contact->has_avatar) {
$htmlString .= '<img src="'.$contact->getAvatarURL(110).'" class="h3 w3 dib tc" width="'.$size.'"></div>';
} else {
if (! is_null($contact->gravatar_url)) {
$htmlString .= '<img src="'.$contact->gravatar_url.'" class="h3 w3 dib tc" width="'.$size.'"></div>';
} else {
if (strlen($contact->getInitials()) == 1) {
$htmlString .= '<div class="h3 w3 dib pt3 white tc f4" style="background-color: '.$contact->getAvatarColor().'">'.$contact->getInitials().'</div></div>';
} else {
$htmlString .= '<div class="h3 w3 dib pt3 white tc f4" style="background-color: '.$contact->getAvatarColor().'">'.$contact->getInitials().'</div></div>';
}
}
}
return $htmlString;
}
}
-18
View File
@@ -1,18 +0,0 @@
<?php
namespace App\Helpers;
use Illuminate\Support\Str;
class RandomHelper
{
/**
* Generate a UUID.
*
* @return string
*/
public static function uuid()
{
return Str::uuid()->toString();
}
}
@@ -2,7 +2,6 @@
namespace App\Http\Controllers;
use App\Helpers\AvatarHelper;
use App\Models\Contact\Contact;
use App\Models\Account\Activity;
use App\Models\Journal\JournalEntry;
@@ -35,7 +34,6 @@ class ActivitiesController extends Controller
{
return view('activities.add')
->withContact($contact)
->withAvatar(AvatarHelper::get($contact, 87))
->withActivity(new Activity);
}
@@ -99,7 +97,6 @@ class ActivitiesController extends Controller
{
return view('activities.edit')
->withContact($contact)
->withAvatar(AvatarHelper::get($contact, 87))
->withActivity($activity);
}
@@ -0,0 +1,90 @@
<?php
namespace App\Http\Controllers\Contacts;
use Illuminate\Http\Request;
use App\Models\Contact\Contact;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use App\Services\Account\Photo\UploadPhoto;
use App\Services\Contact\Avatar\UpdateAvatar;
class AvatarController extends Controller
{
/**
* Display the Edit avatar screen.
*/
public function edit(Contact $contact)
{
return view('people.avatar.edit')
->withContact($contact);
}
/**
* Update the avatar of the contact.
*
* @param Request $request
* @param Contact $contact
*/
public function update(Request $request, Contact $contact)
{
// update the avatar
$data = [
'account_id' => auth()->user()->account->id,
'contact_id' => $contact->id,
'source' => $request->get('avatar'),
];
switch ($request->get('avatar')) {
case 'upload':
// if it's a new photo, we need to upload it
$validator = Validator::make($request->all(), [
'file' => 'image|max:'.config('monica.max_upload_size'),
]);
if ($validator->fails()) {
return back()
->withInput()
->withErrors($validator);
}
$photo = app(UploadPhoto::class)->execute([
'account_id' => auth()->user()->account->id,
'photo' => $request->photo,
]);
$data['photo_id'] = $photo->id;
$data['source'] = 'photo';
break;
case 'photo':
$data['photo_id'] = $contact->avatar_photo_id;
break;
}
app(UpdateAvatar::class)->execute($data);
return redirect()->route('people.show', $contact)
->with('success', trans('people.information_edit_success'));
}
/**
* Set the given photo as avatar.
*
* @param Request $request
* @param Contact $contact
* @param int $photoId
*/
public function photo(Request $request, Contact $contact, $photoId)
{
// update the avatar
$data = [
'account_id' => auth()->user()->account->id,
'contact_id' => $contact->id,
'source' => 'photo',
'photo_id' => $photoId,
];
return app(UpdateAvatar::class)->execute($data);
}
}
@@ -7,6 +7,7 @@ use App\Http\Controllers\Controller;
use App\Models\Contact\ContactField;
use Illuminate\Support\Facades\Auth;
use App\Http\Requests\People\ContactFieldsRequest;
use App\Services\Contact\Avatar\GetAvatarsFromInternet;
class ContactFieldsController extends Controller
{
@@ -57,7 +58,9 @@ class ContactFieldsController extends Controller
]
);
$contact->updateGravatar();
app(GetAvatarsFromInternet::class)->execute([
'contact_id' => $contact->id,
]);
return $contactField;
}
@@ -77,7 +80,9 @@ class ContactFieldsController extends Controller
]
);
$contact->updateGravatar();
app(GetAvatarsFromInternet::class)->execute([
'contact_id' => $contact->id,
]);
return $contactField;
}
@@ -86,6 +91,8 @@ class ContactFieldsController extends Controller
{
$contactField->delete();
$contact->updateGravatar();
app(GetAvatarsFromInternet::class)->execute([
'contact_id' => $contact->id,
]);
}
}
@@ -3,7 +3,6 @@
namespace App\Http\Controllers\Contacts;
use App\Models\Contact\Debt;
use App\Helpers\AvatarHelper;
use App\Models\Contact\Contact;
use App\Http\Controllers\Controller;
use App\Http\Requests\People\DebtRequest;
@@ -34,7 +33,6 @@ class DebtController extends Controller
{
return view('people.debt.add')
->withContact($contact)
->withAvatar(AvatarHelper::get($contact, 87))
->withDebt(new Debt);
}
@@ -89,7 +87,6 @@ class DebtController extends Controller
{
return view('people.debt.edit')
->withContact($contact)
->withAvatar(AvatarHelper::get($contact, 87))
->withDebt($debt);
}
@@ -5,7 +5,6 @@ namespace App\Http\Controllers\Contacts;
use App\Helpers\DateHelper;
use App\Helpers\MoneyHelper;
use App\Models\Contact\Gift;
use App\Helpers\AvatarHelper;
use App\Models\Contact\Contact;
use App\Http\Controllers\Controller;
use App\Http\Requests\People\GiftsRequest;
@@ -82,7 +81,6 @@ class GiftsController extends Controller
return view('people.gifts.add')
->withContact($contact)
->withFamilyRelationships($familyRelationships)
->withAvatar(AvatarHelper::get($contact, 87))
->withGift(new Gift);
}
@@ -117,7 +115,6 @@ class GiftsController extends Controller
return view('people.gifts.edit')
->withContact($contact)
->withFamilyRelationships($familyRelationships)
->withAvatar(AvatarHelper::get($contact, 87))
->withGift($gift);
}
@@ -9,6 +9,7 @@ use App\Http\Controllers\Controller;
use App\Traits\JsonRespondController;
use App\Services\Account\Photo\UploadPhoto;
use App\Services\Account\Photo\DestroyPhoto;
use App\Services\Contact\Avatar\UpdateAvatar;
use App\Http\Resources\Photo\Photo as PhotoResource;
class PhotosController extends Controller
@@ -72,5 +73,14 @@ class PhotosController extends Controller
} catch (\Exception $e) {
return $this->respondNotFound();
}
if ($contact->avatar_source == 'photo'
&& $contact->avatar_photo_id == $photo->id) {
app(UpdateAvatar::class)->execute([
'account_id' => auth()->user()->account->id,
'contact_id' => $contact->id,
'source' => 'adorable',
]);
}
}
}
@@ -3,7 +3,6 @@
namespace App\Http\Controllers\Contacts;
use Illuminate\Http\Request;
use App\Helpers\AvatarHelper;
use App\Models\Contact\Contact;
use App\Models\Contact\Reminder;
use App\Http\Controllers\Controller;
@@ -24,7 +23,6 @@ class RemindersController extends Controller
{
return view('people.reminders.add')
->withContact($contact)
->withAvatar(AvatarHelper::get($contact, 87))
->withReminder(new Reminder);
}
@@ -66,7 +64,6 @@ class RemindersController extends Controller
{
return view('people.reminders.edit')
->withContact($contact)
->withAvatar(AvatarHelper::get($contact, 87))
->withReminder($reminder);
}
@@ -3,11 +3,9 @@
namespace App\Http\Controllers;
use App\Helpers\DateHelper;
use App\Jobs\ResizeAvatars;
use App\Models\Contact\Tag;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use App\Helpers\AvatarHelper;
use App\Helpers\LocaleHelper;
use App\Helpers\SearchHelper;
use App\Helpers\GendersHelper;
@@ -299,7 +297,6 @@ class ContactsController extends Controller
->withWorkRelationships($workRelationships)
->withReminders($reminders)
->withModules($modules)
->withAvatar(AvatarHelper::get($contact, 87))
->withContact($contact)
->withWeather($contact->getWeather())
->withDays($days)
@@ -424,8 +421,6 @@ class ContactsController extends Controller
$contact->save();
}
dispatch(new ResizeAvatars($contact));
return redirect()->route('people.show', $contact)
->with('success', trans('people.information_edit_success'));
}
@@ -502,7 +497,6 @@ class ContactsController extends Controller
public function editFoodPreferences(Request $request, Contact $contact)
{
return view('people.food-preferences.edit')
->withAvatar(AvatarHelper::get($contact, 87))
->withContact($contact);
}
+2 -2
View File
@@ -44,7 +44,7 @@ class DashboardController extends Controller
$data = [
'id' => $contact->hashID(),
'has_avatar' => $contact->has_avatar,
'avatar_url' => $contact->getAvatarURL(110),
'avatar_url' => $contact->getAvatarURL(),
'initials' => $contact->getInitials(),
'default_avatar_color' => $contact->default_avatar_color,
'complete_name' => $contact->name,
@@ -138,7 +138,7 @@ class DashboardController extends Controller
'contact' => [
'id' => $note->contact->hashID(),
'has_avatar' => $note->contact->has_avatar,
'avatar_url' => $note->contact->getAvatarURL(110),
'avatar_url' => $note->contact->getAvatarURL(),
'initials' => $note->contact->getInitials(),
'default_avatar_color' => $note->contact->default_avatar_color,
'complete_name' => $note->contact->name,
@@ -42,6 +42,7 @@ class SettingsController
'api_usage',
'cache',
'countries',
'contact_photo',
'crons',
'currencies',
'contact_photo',
+1 -1
View File
@@ -12,7 +12,7 @@ class ImportsRequest extends AuthorizedRequest
public function rules()
{
return [
'vcard' => 'required|max:'.config('monica.max_upload_size').'|mimes:vcf,vcard',
'vcard' => 'required|file|max:'.config('monica.max_upload_size').'|mimes:vcf,vcard',
];
}
}
+2 -2
View File
@@ -71,8 +71,8 @@ class Contact extends Resource
'company' => $this->company,
]),
'avatar' => $this->when(! $this->is_partial, [
'url' => $this->getAvatarUrl(110),
'source' => $this->getAvatarSource(),
'url' => $this->getAvatarUrl(),
'source' => $this->avatar_source,
'default_avatar_color' => $this->default_avatar_color,
]),
'food_preferences' => $this->when(! $this->is_partial, $this->food_preferences),
+2 -2
View File
@@ -40,8 +40,8 @@ class ContactShort extends Contact
'date' => DateHelper::getTimestamp($this->deceasedDate),
],
'avatar' => [
'has_avatar' => $this->has_avatar,
'avatar_url' => $this->getAvatarURL(110),
'url' => $this->getAvatarUrl(),
'source' => $this->avatar_source,
'default_avatar_color' => $this->default_avatar_color,
],
],
@@ -69,8 +69,8 @@ class ContactWithContactFields extends Contact
'company' => $this->company,
]),
'avatar' => $this->when(! $this->is_partial, [
'url' => $this->getAvatarUrl(110),
'source' => $this->getAvatarSource(),
'url' => $this->getAvatarUrl(),
'source' => $this->avatar_source,
'default_avatar_color' => $this->default_avatar_color,
]),
'food_preferences' => $this->when(! $this->is_partial, $this->food_preferences),
+1 -1
View File
@@ -27,7 +27,7 @@ class Photo extends Resource
'account' => [
'id' => $this->account->id,
],
'contact' => new ContactShortResource($this->contact()),
'contact' => ContactShortResource::collection($this->contacts)[0],
'created_at' => DateHelper::getTimestamp($this->created_at),
'updated_at' => DateHelper::getTimestamp($this->updated_at),
];
@@ -0,0 +1,194 @@
<?php
namespace App\Jobs\Avatars;
use App\Models\Account\Photo;
use Illuminate\Bus\Queueable;
use App\Events\MoveAvatarEvent;
use App\Models\Contact\Contact;
use Illuminate\Support\Facades\Event;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
use Illuminate\Queue\InteractsWithQueue;
use App\Exceptions\FileNotFoundException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use App\Services\Contact\Avatar\UpdateAvatar;
class MoveContactAvatarToPhotosDirectory implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 1;
/**
* @var Contact
*/
private $contact;
/**
* @var bool
*/
private $dryrun;
/**
* @var \Illuminate\Contracts\Filesystem\Filesystem
*/
private $storage;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($contact, $dryrun)
{
$this->contact = $contact;
$this->dryrun = $dryrun;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$this->storage = Storage::disk($this->contact->avatar_location);
// move avatar to new location
$avatarFileName = $this->moveContactAvatars();
if ($this->dryrun) {
return;
}
// create a Photo object for this avatar
$photo = $this->createPhotoObject($avatarFileName);
// associate the Photo object to the contact
$this->associatePhotoAsAvatar($photo);
// delete original avatar
$this->deleteOriginalAvatar($avatarFileName);
// delete thumbnails of avatars
$this->deleteThumbnails();
}
/**
* @return string|null
*/
private function moveContactAvatars(): ?string
{
Event::dispatch(new MoveAvatarEvent($this->contact));
$newStorage = Storage::disk(config('filesystems.default'));
$avatarFileName = $this->getAvatarFileName();
// $avatarFileName has the format `avatars/XXX.jpg`. We need to remove
// the `avatars/` string to store the new file.
$newAvatarFilename = str_replace('avatars/', 'photos/', $avatarFileName);
if ($newStorage->exists($newAvatarFilename)) {
return null;
}
if (! $this->dryrun) {
$avatarFile = $this->storage->get($avatarFileName);
$newStorage->put($newAvatarFilename, $avatarFile, 'public');
$this->contact->avatar_location = config('filesystems.default');
$this->contact->save();
}
return $avatarFileName;
}
/**
* @param string|null $avatarFileName
* @return Photo|null
*/
private function createPhotoObject($avatarFileName)
{
if (is_null($avatarFileName)) {
return;
}
$newAvatarFilename = str_replace('avatars/', '', $avatarFileName);
$photo = new Photo;
$photo->account_id = $this->contact->account_id;
$photo->original_filename = $newAvatarFilename;
$photo->new_filename = 'photos/'.$newAvatarFilename;
$photo->filesize = Storage::disk($this->contact->avatar_location)->size('/photos/'.$newAvatarFilename);
$photo->mime_type = 'adfad';
$photo->save();
return $photo;
}
private function associatePhotoAsAvatar($photo)
{
if (is_null($photo)) {
return;
}
$data = [
'account_id' => $this->contact->account_id,
'contact_id' => $this->contact->id,
'source' => 'photo',
'photo_id' => $photo->id,
];
app(UpdateAvatar::class)->execute($data);
}
private function deleteThumbnails()
{
try {
$smallThumbnail = $this->getAvatarFileName(110);
$this->storage->delete($smallThumbnail);
} catch (FileNotFoundException $e) {
// ignore
}
try {
$bigThumbnail = $this->getAvatarFileName(174);
$this->storage->delete($bigThumbnail);
} catch (FileNotFoundException $e) {
// ignore
}
}
private function deleteOriginalAvatar($avatarFileName)
{
$this->storage->delete($avatarFileName);
}
private function getAvatarFileName($size = null)
{
$filename = pathinfo($this->contact->avatar_file_name, PATHINFO_FILENAME);
$extension = pathinfo($this->contact->avatar_file_name, PATHINFO_EXTENSION);
$avatarFileName = 'avatars/'.$filename.'.'.$extension;
if (! is_null($size)) {
$avatarFileName = 'avatars/'.$filename.'_'.$size.'.'.$extension;
}
if (! $this->fileExists($avatarFileName)) {
throw new FileNotFoundException($avatarFileName);
}
return $avatarFileName;
}
private function fileExists($avatarFileName) : bool
{
return $this->storage->exists($avatarFileName);
}
}
+1 -1
View File
@@ -42,7 +42,7 @@ class Photo extends Model
}
/**
* Get the contact record associated with the photo.
* Get the first contact record associated with the photo.
*
* @return Contact
*/
+40 -100
View File
@@ -65,16 +65,17 @@ class Contact extends Model
'middle_name',
'last_name',
'nickname',
'has_avatar',
'avatar_file_name',
'gravatar_url',
'avatar_external_url',
'default_avatar_color',
'gender_id',
'account_id',
'created_at',
'updated_at',
'is_partial',
'avatar_source',
'avatar_adorable_url',
'avatar_gravatar_url',
'avatar_default_url',
'avatar_photo_id',
'default_avatar_color',
];
/**
@@ -97,7 +98,6 @@ class Contact extends Model
'birthday_reminder_id',
'birthday_special_date_id',
'is_dead',
'avatar_external_url',
'last_consulted_at',
'created_at',
'first_met_additional_info',
@@ -796,16 +796,6 @@ class Contact extends Model
return $contacts;
}
/**
* Get the default color of the avatar if no picture is present.
*
* @return string
*/
public function getAvatarColor()
{
return $this->default_avatar_color;
}
/**
* Set the default avatar color for this object.
*
@@ -946,28 +936,46 @@ class Contact extends Model
}
/**
* Returns the URL of the avatar with the given size.
* Get the default avatar URL.
*
* @return string
*/
public function getAvatarDefaultURL()
{
return asset(Storage::disk(config('filesystems.default'))->url($this->avatar_default_url));
}
/**
* Returns the URL of the avatar, properly sized.
* The avatar can come from 4 sources:
* - default,
* - Adorable avatar,
* - Gravatar
* - or a photo that has been uploaded.
*
* @param int $size
* @return string|null
*/
public function getAvatarURL($size = 110)
public function getAvatarURL()
{
// it either returns null or the gravatar url if it's defined
if (! $this->has_avatar) {
return $this->gravatar_url;
$avatarURL = '';
switch ($this->avatar_source) {
case 'adorable':
$avatarURL = $this->avatar_adorable_url;
break;
case 'gravatar':
$avatarURL = $this->avatar_gravatar_url;
break;
case 'photo':
$avatarURL = Photo::find($this->avatar_photo_id)->url();
break;
case 'default':
default:
$avatarURL = $this->getAvatarDefaultURL();
break;
}
if ($this->avatar_location == 'external') {
return $this->avatar_external_url;
}
$originalAvatarUrl = $this->avatar_file_name;
$avatarFilename = pathinfo($originalAvatarUrl, PATHINFO_FILENAME);
$avatarExtension = pathinfo($originalAvatarUrl, PATHINFO_EXTENSION);
$resizedAvatar = 'avatars/'.$avatarFilename.'_'.$size.'.'.$avatarExtension;
return asset(Storage::disk($this->avatar_location)->url($resizedAvatar));
return $avatarURL;
}
/**
@@ -1011,74 +1019,6 @@ class Contact extends Model
}
}
/**
* Returns the source of the avatar, or null if avatar is undefined.
*/
public function getAvatarSource()
{
if (! $this->has_avatar) {
return;
}
if ($this->avatar_location == 'external') {
return 'external';
}
return 'internal';
}
/**
* Update the gravatar, using the firt email found.
*/
public function updateGravatar()
{
// for performance reasons, we check if a gravatar exists for this email
// address. if it does, we store the gravatar url in the database.
// while this is not ideal because the gravatar can change, at least we
// won't make constant call to gravatar to load the avatar on every
// page load.
$response = $this->getGravatar(250);
if ($response !== false && is_string($response)) {
$this->gravatar_url = $response;
} else {
$this->gravatar_url = null;
}
$this->save();
}
/**
* Get the first gravatar of all emails, if found.
*
* @param int $size
* @return string|bool
*/
public function getGravatar($size)
{
$emails = $this->contactFields()->email()->get();
foreach ($emails as $email) {
if (empty($email) || empty($email->data)) {
continue;
}
try {
if (! app('gravatar')->exists($email->data)) {
continue;
}
} catch (\Creativeorange\Gravatar\Exceptions\InvalidEmailException $e) {
// catch invalid email
continue;
}
return app('gravatar')->get($email->data, [
'size' => $size,
'secure' => config('app.env') === 'production',
]);
}
return false;
}
/**
* Check if the contact has debt (by the contact or the user for this contact).
*
+5
View File
@@ -79,6 +79,11 @@ class AppServiceProvider extends ServiceProvider
\App\Services\Auth\Population\PopulateContactFieldTypesTable::class => \App\Services\Auth\Population\PopulateContactFieldTypesTable::class,
\App\Services\Auth\Population\PopulateLifeEventsTable::class => \App\Services\Auth\Population\PopulateLifeEventsTable::class,
\App\Services\Auth\Population\PopulateModulesTable::class => \App\Services\Auth\Population\PopulateModulesTable::class,
\App\Services\Contact\Avatar\GenerateDefaultAvatar::class => \App\Services\Contact\Avatar\GenerateDefaultAvatar::class,
\App\Services\Contact\Avatar\GetAdorableAvatarURL::class => \App\Services\Contact\Avatar\GetAdorableAvatarURL::class,
\App\Services\Contact\Avatar\GetAvatarsFromInternet::class => \App\Services\Contact\Avatar\GetAvatarsFromInternet::class,
\App\Services\Contact\Avatar\GetGravatarURL::class => \App\Services\Contact\Avatar\GetGravatarURL::class,
\App\Services\Contact\Avatar\UpdateAvatar::class => \App\Services\Contact\Avatar\UpdateAvatar::class,
\App\Services\Contact\Address\CreateAddress::class => \App\Services\Contact\Address\CreateAddress::class,
\App\Services\Contact\Address\DestroyAddress::class => \App\Services\Contact\Address\DestroyAddress::class,
\App\Services\Contact\Address\UpdateAddress::class => \App\Services\Contact\Address\UpdateAddress::class,
+4 -1
View File
@@ -17,7 +17,7 @@ class DestroyPhoto extends BaseService
{
return [
'account_id' => 'required|integer|exists:accounts,id',
'photo_id' => 'required|integer',
'photo_id' => 'required|integer|exists:photos,id',
];
}
@@ -30,11 +30,14 @@ class DestroyPhoto extends BaseService
public function execute(array $data) : bool
{
$this->validate($data);
$photo = Photo::where('account_id', $data['account_id'])
->findOrFail($data['photo_id']);
// Delete the physical photo
// Throws FileNotFoundException
Storage::delete($photo->new_filename);
// Delete the object in the DB
$photo->delete();
@@ -0,0 +1,111 @@
<?php
namespace App\Services\Contact\Avatar;
use Illuminate\Support\Str;
use App\Services\BaseService;
use App\Models\Contact\Contact;
use Laravolt\Avatar\Facade as Avatar;
use Illuminate\Support\Facades\Storage;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
class GenerateDefaultAvatar extends BaseService
{
/**
* Get the validation rules that apply to the service.
*
* @return array
*/
public function rules()
{
return [
'contact_id' => 'required|integer|exists:contacts,id',
];
}
/**
* Generate the default image for the avatar, based on the initals of the
* contact and returns the filename.
*
* @param array $data
* @return Contact
*/
public function execute(array $data)
{
$this->validate($data);
$contact = Contact::find($data['contact_id']);
$contact = $this->generateContactUUID($contact);
// delete existing default avatar
$this->deleteExistingDefaultAvatar($contact);
// create new avatar
$filename = $this->createNewAvatar($contact);
$contact->avatar_default_url = $filename;
$contact->save();
return $contact;
}
/**
* Create an uuid for the contact if it does not exist.
*
* @param Contact $contact
* @return Contact
*/
private function generateContactUUID(Contact $contact)
{
if (! $contact->uuid) {
$contact->uuid = Str::uuid()->toString();
$contact->save();
}
return $contact;
}
/**
* Create a new avatar for the contact based on the name of the contact.
*
* @param Contact $contact
* @return string
*/
private function createNewAvatar(Contact $contact)
{
$img = null;
try {
$img = Avatar::create($contact->name)
->setBackground($contact->default_avatar_color)
->getImageObject()
->encode('jpg');
$filename = 'avatars/'.$contact->uuid.'.jpg';
Storage::put($filename, $img);
return $filename;
} finally {
if ($img) {
$img->destroy();
}
}
}
/**
* Delete the existing default avatar.
*
* @param Contact $contact
* @return void
*/
private function deleteExistingDefaultAvatar(Contact $contact)
{
try {
Storage::delete($contact->avatar_default_url);
$contact->avatar_default_url = null;
$contact->save();
} catch (FileNotFoundException $e) {
return;
}
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Services\Contact\Avatar;
use App\Services\BaseService;
class GetAdorableAvatarURL extends BaseService
{
private const ADORABLE_API = 'https://api.adorable.io/avatars/';
/**
* Get the validation rules that apply to the service.
*
* @return array
*/
public function rules()
{
return [
'uuid' => 'required|string',
'size' => 'nullable|integer|between:1,2000',
];
}
/**
* Get an url for an adorable avatar.
* - http://avatars.adorable.io/ gives avatars based on a random string.
*
* @param array $data
* @return string|null
*/
public function execute(array $data)
{
$this->validate($data);
$size = $this->size($data);
return self::ADORABLE_API.$size.'/'.$data['uuid'].'.png';
}
/**
* Get the size for the avatar, based on a given parameter. Provides a
* default otherwise.
*
* @param array $data
* @return int
*/
private function size(array $data)
{
if (isset($data['size'])) {
return $data['size'];
}
return (int) config('monica.avatar_size');
}
}
@@ -0,0 +1,125 @@
<?php
namespace App\Services\Contact\Avatar;
use Illuminate\Support\Str;
use App\Services\BaseService;
use App\Models\Contact\Contact;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class GetAvatarsFromInternet extends BaseService
{
/**
* Get the validation rules that apply to the service.
*
* @return array
*/
public function rules()
{
return [
'contact_id' => 'required|integer|exists:contacts,id',
];
}
/**
* Query both Gravatar and Adorable Avatars based on the email address of
* the contact.
*
* - http://avatars.adorable.io/ gives avatars based on a random string.
* This random string comes from the `avatar_adorable_uuid` field in the
* Contact object.
* - Gravatar only gives an avatar only if it's set.
*
* @param array $data
* @return Contact
*/
public function execute(array $data): Contact
{
$this->validate($data);
$contact = Contact::findOrFail($data['contact_id']);
$contact = $this->generateUUID($contact);
$this->getAdorable($contact);
$this->getGravatar($contact);
return $contact;
}
/**
* Generate the UUID used to identify the contact in the Adorable service.
*
* @param Contact $contact
* @return Contact
*/
private function generateUUID(Contact $contact)
{
if (empty($contact->avatar_adorable_uuid)) {
$contact->avatar_adorable_uuid = Str::uuid()->toString();
$contact->save();
}
return $contact;
}
/**
* Get the adorable avatar.
*
* @param Contact $contact
* @return void
*/
private function getAdorable(Contact $contact)
{
$contact->avatar_adorable_url = app(GetAdorableAvatarURL::class)->execute([
'uuid' => $contact->avatar_adorable_uuid,
'size' => 200,
]);
$contact->save();
}
/**
* Get the email (if it exists) of the contact, based on the contact fields.
*
* @param Contact $contact
* @return null|string
*/
private function getEmail(Contact $contact)
{
try {
$contactField = $contact->contactFields()
->email()
->first();
return $contactField ? $contactField->data : null;
} catch (ModelNotFoundException $e) {
// Not found
}
}
/**
* Query Gravatar (if it exists) for the contact's email address.
*
* @param Contact $contact
* @return void
*/
private function getGravatar(Contact $contact)
{
$email = $this->getEmail($contact);
if ($email) {
$contact->avatar_gravatar_url = app(GetGravatarURL::class)->execute([
'email' => $email,
'size' => config('monica.avatar_size'),
]);
} else {
// in this case we need to make sure that we reset the gravatar URL
$contact->avatar_gravatar_url = null;
if ($contact->avatar_source == 'gravatar') {
$contact->avatar_source = 'adorable';
}
}
$contact->save();
}
}
@@ -0,0 +1,75 @@
<?php
namespace App\Services\Contact\Avatar;
use App\Services\BaseService;
use Illuminate\Support\Facades\App;
use Creativeorange\Gravatar\Facades\Gravatar;
class GetGravatarURL extends BaseService
{
/**
* Get the validation rules that apply to the service.
*
* @return array
*/
public function rules()
{
return [
'email' => 'required|email',
'size' => 'nullable|integer|between:1,2000',
];
}
/**
* Get Gravatar, if it exists.
*
* @param array $data
* @return string|null
*/
public function execute(array $data)
{
$this->validate($data);
if ($this->exists($data)) {
$size = $this->size($data);
return Gravatar::get($data['email'], [
'size' => $size,
'secure' => App::environment('production'),
]);
}
}
/**
* Test given email.
*
* @param array $data
* @return bool
*/
private function exists(array $data)
{
try {
return Gravatar::exists($data['email']);
} catch (\Creativeorange\Gravatar\Exceptions\InvalidEmailException $e) {
// catch invalid email
return false;
}
}
/**
* Get the size for the gravatar, based on a given parameter. Provides a
* default otherwise.
*
* @param array $data
* @return int
*/
private function size(array $data)
{
if (isset($data['size'])) {
return $data['size'];
}
return (int) config('monica.avatar_size');
}
}
@@ -0,0 +1,69 @@
<?php
namespace App\Services\Contact\Avatar;
use App\Models\Account\Photo;
use App\Services\BaseService;
use App\Models\Contact\Contact;
use Illuminate\Validation\Rule;
/**
* Update the avatar of the contact.
*/
class UpdateAvatar extends BaseService
{
/**
* Get the validation rules that apply to the service.
*
* @return array
*/
public function rules()
{
return [
'account_id' => 'required|integer|exists:accounts,id',
'contact_id' => 'required|integer|exists:contacts,id',
'source' => [
'required',
Rule::in([
'default',
'adorable',
'gravatar',
'photo',
]),
],
'photo_id' => 'required_if:source,photo|integer',
];
}
/**
* Update message in a conversation.
*
* @param array $data
* @return Contact
*/
public function execute(array $data) : Contact
{
$this->validate($data);
$contact = Contact::where('account_id', $data['account_id'])
->findOrFail($data['contact_id']);
if (isset($data['photo_id'])) {
Photo::where('account_id', $data['account_id'])
->findOrFail($data['photo_id']);
}
$contact->avatar_source = $data['source'];
$contact->avatar_photo_id = null;
// in case of a photo, set the photo as the avatar
if ($data['source'] === 'photo') {
$contact->avatar_photo_id = $data['photo_id'];
$contact->photos()->syncWithoutDetaching([$data['photo_id']]);
}
$contact->save();
return $contact;
}
}
+28 -5
View File
@@ -3,9 +3,11 @@
namespace App\Services\Contact\Contact;
use Illuminate\Support\Arr;
use App\Helpers\RandomHelper;
use Illuminate\Support\Str;
use App\Services\BaseService;
use App\Models\Contact\Contact;
use App\Services\Contact\Avatar\GenerateDefaultAvatar;
use App\Services\Contact\Avatar\GetAvatarsFromInternet;
class CreateContact extends BaseService
{
@@ -50,7 +52,6 @@ class CreateContact extends BaseService
public function execute(array $data) : Contact
{
$this->validate($data);
// filter out the data that shall not be updated here
$dataOnly = Arr::except(
$data,
@@ -79,8 +80,7 @@ class CreateContact extends BaseService
$this->generateUUID($contact);
$contact->setAvatarColor();
$contact->save();
$this->addAvatars($contact);
// we query the DB again to fill the object with all the new properties
$contact->refresh();
@@ -96,10 +96,33 @@ class CreateContact extends BaseService
*/
private function generateUUID(Contact $contact)
{
$contact->uuid = RandomHelper::uuid();
$contact->uuid = Str::uuid()->toString();
$contact->save();
}
/**
* Add the different default avatars.
*
* @param Contact $contact
* @return void
*/
private function addAvatars(Contact $contact)
{
// set the default avatar color
$contact->setAvatarColor();
$contact->save();
// populate the avatar from Adorable and grab the Gravatar
app(GetAvatarsFromInternet::class)->execute([
'contact_id' => $contact->id,
]);
// also generate the default avatar
app(GenerateDefaultAvatar::class)->execute([
'contact_id' => $contact->id,
]);
}
/**
* Update the information about the birthday.
*
+21 -2
View File
@@ -5,6 +5,7 @@ namespace App\Services\Contact\Contact;
use Illuminate\Support\Arr;
use App\Services\BaseService;
use App\Models\Contact\Contact;
use App\Services\Contact\Avatar\GenerateDefaultAvatar;
class UpdateContact extends BaseService
{
@@ -60,8 +61,6 @@ class UpdateContact extends BaseService
$this->updateDeceasedInformation($data, $contact);
$contact->updateGravatar();
// we query the DB again to fill the object with all the new properties
$contact->refresh();
@@ -97,7 +96,27 @@ class UpdateContact extends BaseService
]
);
$oldName = $contact->name;
$contact->update($dataOnly);
// only update the avatar if the name has changed
if ($oldName != $contact->name) {
$this->updateDefaultAvatar($contact);
}
}
/**
* Update the default avatar.
*
* @param Contact $contact
* @return void
*/
private function updateDefaultAvatar(Contact $contact)
{
app(GenerateDefaultAvatar::class)->execute([
'contact_id' => $contact->id,
]);
}
/**
+2 -1
View File
@@ -130,7 +130,8 @@ class ExportVCard extends BaseService
private function exportPhoto(Contact $contact, VCard $vcard)
{
$picture = $contact->getAvatarURL();
if (! is_null($picture)) {
if (! empty($picture)) {
$vcard->add('PHOTO', $picture);
}
}
+2 -3
View File
@@ -12,7 +12,6 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use App\Helpers\VCardHelper;
use App\Helpers\LocaleHelper;
use App\Helpers\RandomHelper;
use App\Services\BaseService;
use function Safe\preg_split;
use App\Models\Contact\Gender;
@@ -436,7 +435,7 @@ class ImportVCard extends BaseService
$contact->account_id = $this->accountId;
$contact->gender_id = $this->getGender('O')->id;
$contact->setAvatarColor();
$contact->uuid = RandomHelper::uuid();
$contact->uuid = Str::uuid()->toString();
$contact->save();
}
@@ -616,7 +615,7 @@ class ImportVCard extends BaseService
if ($entry->PHOTO instanceof \Sabre\VObject\Property\Uri) {
if (Str::startsWith((string) $entry->PHOTO, 'https://secure.gravatar.com') || Str::startsWith((string) $entry->PHOTO, 'https://www.gravatar.com')) {
// Gravatar
$contact->gravatar_url = (string) $entry->PHOTO;
$contact->avatar_gravatar_url = (string) $entry->PHOTO;
} else {
// assume monica asset
}
+1
View File
@@ -26,6 +26,7 @@
"laravel/passport": "^7.2",
"laravel/socialite": "^4.0",
"laravel/tinker": "^1.0",
"laravolt/avatar": "^2.2",
"league/flysystem-aws-s3-v3": "~1.0",
"league/flysystem-cached-adapter": "^1.0",
"mariuzzo/laravel-js-localization": "^1.4",
Generated
+225 -103
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "664704dc93f7b927108e8e86e5da9705",
"content-hash": "f31dcc2eea2046360d1ff08a8fb2fd4b",
"packages": [
{
"name": "asbiin/laravel-webauthn",
@@ -197,9 +197,9 @@
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"role": "Developer",
"email": "mail@dasprids.de",
"homepage": "http://www.dasprids.de"
"homepage": "http://www.dasprids.de",
"role": "Developer"
}
],
"description": "BaconQrCode is a QR code generator for PHP.",
@@ -362,9 +362,9 @@
"authors": [
{
"name": "Colin O'Dell",
"role": "Developer",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com"
"homepage": "https://www.colinodell.com",
"role": "Developer"
}
],
"description": "UTF-8 compatible JSON5 parser for PHP",
@@ -474,9 +474,9 @@
"authors": [
{
"name": "Jaco Tijssen",
"role": "Developer",
"email": "jaco@creativeorange.nl",
"homepage": "https://www.creativeorange.nl"
"homepage": "https://www.creativeorange.nl",
"role": "Developer"
}
],
"description": "Gravatar for Laravel 5.0.x through 5.5.x package for retrieving gravatar image URLs or checking the existance of an image.",
@@ -487,6 +487,62 @@
],
"time": "2019-06-03T08:45:09+00:00"
},
{
"name": "danielstjules/stringy",
"version": "3.1.0",
"source": {
"type": "git",
"url": "https://github.com/danielstjules/Stringy.git",
"reference": "df24ab62d2d8213bbbe88cc36fc35a4503b4bd7e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/danielstjules/Stringy/zipball/df24ab62d2d8213bbbe88cc36fc35a4503b4bd7e",
"reference": "df24ab62d2d8213bbbe88cc36fc35a4503b4bd7e",
"shasum": ""
},
"require": {
"php": ">=5.4.0",
"symfony/polyfill-mbstring": "~1.1"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Stringy\\": "src/"
},
"files": [
"src/Create.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Daniel St. Jules",
"email": "danielst.jules@gmail.com",
"homepage": "http://www.danielstjules.com"
}
],
"description": "A string manipulation library with multibyte support",
"homepage": "https://github.com/danielstjules/Stringy",
"keywords": [
"UTF",
"helpers",
"manipulation",
"methods",
"multibyte",
"string",
"utf-8",
"utility",
"utils"
],
"time": "2017-06-12T01:10:27+00:00"
},
{
"name": "defuse/php-encryption",
"version": "v2.2.1",
@@ -1210,9 +1266,9 @@
"authors": [
{
"name": "Friedrich Große",
"role": "Author",
"email": "friedrich.grosse@gmail.com",
"homepage": "https://github.com/FGrosse"
"homepage": "https://github.com/FGrosse",
"role": "Author"
},
{
"name": "All contributors",
@@ -1323,13 +1379,13 @@
"authors": [
{
"name": "Neuman Vong",
"role": "Developer",
"email": "neuman+pear@twilio.com"
"email": "neuman+pear@twilio.com",
"role": "Developer"
},
{
"name": "Anant Narayanan",
"role": "Developer",
"email": "anant@php.net"
"email": "anant@php.net",
"role": "Developer"
}
],
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
@@ -2628,6 +2684,72 @@
],
"time": "2019-08-07T15:10:45+00:00"
},
{
"name": "laravolt/avatar",
"version": "2.2.1",
"source": {
"type": "git",
"url": "https://github.com/laravolt/avatar.git",
"reference": "58470dbbac0704772d87e775ac60dcd1580f022a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravolt/avatar/zipball/58470dbbac0704772d87e775ac60dcd1580f022a",
"reference": "58470dbbac0704772d87e775ac60dcd1580f022a",
"shasum": ""
},
"require": {
"danielstjules/stringy": "~3.1",
"illuminate/cache": "~5.2",
"illuminate/support": "~5.2",
"intervention/image": "^2.1",
"php": ">=7.0"
},
"require-dev": {
"mockery/mockery": "^0.9.1",
"phpunit/phpunit": "~6.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
},
"laravel": {
"providers": [
"Laravolt\\Avatar\\ServiceProvider"
],
"aliases": {
"Avatar": "Laravolt\\Avatar\\Facade"
}
}
},
"autoload": {
"psr-4": {
"Laravolt\\Avatar\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bayu Hendra Winata",
"role": "Developer",
"email": "uyab.exe@gmail.com",
"homepage": "http://id-laravel.com"
}
],
"description": "Turn name, email, and any other string into initial-based avatar or gravatar.",
"homepage": "https://github.com/laravolt/avatar",
"keywords": [
"avatar",
"gravatar",
"laravel",
"laravolt"
],
"time": "2019-05-24T23:46:54+00:00"
},
{
"name": "lcobucci/jwt",
"version": "3.3.1",
@@ -2672,8 +2794,8 @@
"authors": [
{
"name": "Luís Otávio Cobucci Oblonczyk",
"role": "Developer",
"email": "lcobucci@gmail.com"
"email": "lcobucci@gmail.com",
"role": "Developer"
}
],
"description": "A simple library to work with JSON Web Token and JSON Web Signature",
@@ -2952,9 +3074,9 @@
"authors": [
{
"name": "Ben Corlett",
"role": "Developer",
"email": "bencorlett@me.com",
"homepage": "http://www.webcomm.com.au"
"homepage": "http://www.webcomm.com.au",
"role": "Developer"
}
],
"description": "OAuth 1.0 Client Library",
@@ -3021,15 +3143,15 @@
"authors": [
{
"name": "Alex Bilbie",
"role": "Developer",
"email": "hello@alexbilbie.com",
"homepage": "http://www.alexbilbie.com"
"homepage": "http://www.alexbilbie.com",
"role": "Developer"
},
{
"name": "Andy Millington",
"role": "Developer",
"email": "andrew@noexceptions.io",
"homepage": "https://www.noexceptions.io"
"homepage": "https://www.noexceptions.io",
"role": "Developer"
}
],
"description": "A lightweight and powerful OAuth 2.0 authorization and resource server library with support for all the core specification grants. This library will allow you to secure your API with OAuth and allow your applications users to approve apps that want to access their data from your API.",
@@ -3095,30 +3217,30 @@
"authors": [
{
"name": "Rubens Mariuzzo",
"role": "Developer",
"email": "rubens@mariuzzo.com",
"homepage": "https://github.com/rmariuzzo"
"homepage": "https://github.com/rmariuzzo",
"role": "Developer"
},
{
"name": "German Popoter",
"role": "Developer",
"email": "me@gpopoteur.com",
"homepage": "https://github.com/gpopoteur"
"homepage": "https://github.com/gpopoteur",
"role": "Developer"
},
{
"name": "Galievskiy Dmitriy",
"role": "Developer",
"homepage": "https://github.com/xAockd"
"homepage": "https://github.com/xAockd",
"role": "Developer"
},
{
"name": "Ramon Ackermann",
"role": "Developer",
"homepage": "https://github.com/sboo"
"homepage": "https://github.com/sboo",
"role": "Developer"
},
{
"name": "Pe Ell",
"role": "Developer",
"homepage": "https://github.com/a-komarev"
"homepage": "https://github.com/a-komarev",
"role": "Developer"
}
],
"description": "Laravel Localization in JavaScript",
@@ -4128,15 +4250,15 @@
"authors": [
{
"name": "Paragon Initiative Enterprises",
"role": "Maintainer",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com"
"homepage": "https://paragonie.com",
"role": "Maintainer"
},
{
"name": "Steve 'Sc00bz' Thomas",
"role": "Original Developer",
"email": "steve@tobtu.com",
"homepage": "https://www.tobtu.com"
"homepage": "https://www.tobtu.com",
"role": "Original Developer"
}
],
"description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
@@ -4880,9 +5002,9 @@
"authors": [
{
"name": "Antonio Carlos Ribeiro",
"role": "Creator",
"email": "acr@antoniocarlosribeiro.com",
"homepage": "https://antoniocarlosribeiro.com"
"homepage": "https://antoniocarlosribeiro.com",
"role": "Creator"
}
],
"description": "Laravel Illuminate collection with objectified properties",
@@ -4937,8 +5059,8 @@
"authors": [
{
"name": "Antonio Carlos Ribeiro",
"role": "Creator",
"email": "acr@antoniocarlosribeiro.com"
"email": "acr@antoniocarlosribeiro.com",
"role": "Creator"
}
],
"description": "PHP Countries and Currencies",
@@ -5007,8 +5129,8 @@
"authors": [
{
"name": "Antonio Carlos Ribeiro",
"role": "Creator",
"email": "acr@antoniocarlosribeiro.com"
"email": "acr@antoniocarlosribeiro.com",
"role": "Creator"
}
],
"description": "Countries for Laravel",
@@ -5070,8 +5192,8 @@
"authors": [
{
"name": "Antonio Carlos Ribeiro",
"role": "Creator & Designer",
"email": "acr@antoniocarlosribeiro.com"
"email": "acr@antoniocarlosribeiro.com",
"role": "Creator & Designer"
}
],
"description": "A One Time Password Authentication package, compatible with Google Authenticator.",
@@ -5141,8 +5263,8 @@
"authors": [
{
"name": "Antonio Carlos Ribeiro",
"role": "Creator & Designer",
"email": "acr@antoniocarlosribeiro.com"
"email": "acr@antoniocarlosribeiro.com",
"role": "Creator & Designer"
}
],
"description": "A One Time Password Authentication package, compatible with Google Authenticator.",
@@ -5197,8 +5319,8 @@
"authors": [
{
"name": "Antonio Carlos Ribeiro",
"role": "Creator & Designer",
"email": "acr@antoniocarlosribeiro.com"
"email": "acr@antoniocarlosribeiro.com",
"role": "Creator & Designer"
}
],
"description": "QR Code package for Google2FA",
@@ -5262,9 +5384,9 @@
"authors": [
{
"name": "Antonio Carlos Ribeiro",
"role": "Creator",
"email": "acr@antoniocarlosribeiro.com",
"homepage": "https://antoniocarlosribeiro.com"
"homepage": "https://antoniocarlosribeiro.com",
"role": "Creator"
}
],
"description": "Laravel Illuminate Agnostic Arr",
@@ -5327,9 +5449,9 @@
"authors": [
{
"name": "Antonio Carlos Ribeiro",
"role": "Creator",
"email": "acr@antoniocarlosribeiro.com",
"homepage": "https://antoniocarlosribeiro.com"
"homepage": "https://antoniocarlosribeiro.com",
"role": "Creator"
}
],
"description": "Laravel Illuminate Agnostic Collection",
@@ -5398,9 +5520,9 @@
"authors": [
{
"name": "Antonio Carlos Ribeiro",
"role": "Creator",
"email": "acr@antoniocarlosribeiro.com",
"homepage": "https://antoniocarlosribeiro.com"
"homepage": "https://antoniocarlosribeiro.com",
"role": "Creator"
}
],
"description": "Laravel Illuminate Agnostic Str",
@@ -5459,9 +5581,9 @@
"authors": [
{
"name": "Antonio Carlos Ribeiro",
"role": "Developer",
"email": "acr@antoniocarlosribeiro.com",
"homepage": "https://antoniocarlosribeiro.com"
"homepage": "https://antoniocarlosribeiro.com",
"role": "Developer"
}
],
"description": "Create random chars, numbers, strings",
@@ -5521,9 +5643,9 @@
"authors": [
{
"name": "Antonio Carlos Ribeiro",
"role": "Developer",
"email": "acr@antoniocarlosribeiro.com",
"homepage": "https://antoniocarlosribeiro.com"
"homepage": "https://antoniocarlosribeiro.com",
"role": "Developer"
}
],
"description": "Create recovery codes for two factor auth",
@@ -6394,9 +6516,9 @@
"authors": [
{
"name": "Evert Pot",
"role": "Developer",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/"
"homepage": "http://evertpot.com/",
"role": "Developer"
}
],
"description": "Functions for making sense out of URIs.",
@@ -6455,21 +6577,21 @@
"authors": [
{
"name": "Evert Pot",
"role": "Developer",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/"
"homepage": "http://evertpot.com/",
"role": "Developer"
},
{
"name": "Dominik Tobschall",
"role": "Developer",
"email": "dominik@fruux.com",
"homepage": "http://tobschall.de/"
"homepage": "http://tobschall.de/",
"role": "Developer"
},
{
"name": "Ivan Enderlin",
"role": "Developer",
"email": "ivan.enderlin@hoa-project.net",
"homepage": "http://mnt.io/"
"homepage": "http://mnt.io/",
"role": "Developer"
}
],
"description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
@@ -8932,8 +9054,8 @@
"authors": [
{
"name": "Tijs Verkoyen",
"role": "Developer",
"email": "css_to_inline_styles@verkoyen.eu"
"email": "css_to_inline_styles@verkoyen.eu",
"role": "Developer"
}
],
"description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.",
@@ -8978,13 +9100,13 @@
"authors": [
{
"name": "Daniel Bruce",
"role": "Developer",
"email": "dbruce@vectorface.com"
"email": "dbruce@vectorface.com",
"role": "Developer"
},
{
"name": "Cory Darby",
"role": "Developer",
"email": "ckdarby@vectorface.com"
"email": "ckdarby@vectorface.com",
"role": "Developer"
}
],
"description": "A PHP class for retrieving accurate IP address information for the client.",
@@ -10930,9 +11052,9 @@
"authors": [
{
"name": "Ashot Khanamiryan",
"role": "Developer",
"email": "a.khanamiryan@gmail.com",
"homepage": "https://github.com/khanamiryan"
"homepage": "https://github.com/khanamiryan",
"role": "Developer"
}
],
"description": "QR code decoder / reader",
@@ -11974,18 +12096,18 @@
"authors": [
{
"name": "Arne Blankerts",
"role": "Developer",
"email": "arne@blankerts.de"
"email": "arne@blankerts.de",
"role": "Developer"
},
{
"name": "Sebastian Heuer",
"role": "Developer",
"email": "sebastian@phpeople.de"
"email": "sebastian@phpeople.de",
"role": "Developer"
},
{
"name": "Sebastian Bergmann",
"role": "Developer",
"email": "sebastian@phpunit.de"
"email": "sebastian@phpunit.de",
"role": "Developer"
}
],
"description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
@@ -12021,18 +12143,18 @@
"authors": [
{
"name": "Arne Blankerts",
"role": "Developer",
"email": "arne@blankerts.de"
"email": "arne@blankerts.de",
"role": "Developer"
},
{
"name": "Sebastian Heuer",
"role": "Developer",
"email": "sebastian@phpeople.de"
"email": "sebastian@phpeople.de",
"role": "Developer"
},
{
"name": "Sebastian Bergmann",
"role": "Developer",
"email": "sebastian@phpunit.de"
"email": "sebastian@phpunit.de",
"role": "Developer"
}
],
"description": "Library for handling version information and constraints",
@@ -12475,8 +12597,8 @@
"authors": [
{
"name": "Sebastian Bergmann",
"role": "lead",
"email": "sebastian@phpunit.de"
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "FilterIterator implementation that filters files based on a list of suffixes.",
@@ -12517,8 +12639,8 @@
"authors": [
{
"name": "Sebastian Bergmann",
"role": "lead",
"email": "sebastian@phpunit.de"
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "Simple template engine.",
@@ -12566,8 +12688,8 @@
"authors": [
{
"name": "Sebastian Bergmann",
"role": "lead",
"email": "sebastian@phpunit.de"
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "Utility class for timing",
@@ -12670,8 +12792,8 @@
"authors": [
{
"name": "Sebastian Bergmann",
"role": "lead",
"email": "sebastian@phpunit.de"
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "CLI frontend for php-code-coverage",
@@ -13335,8 +13457,8 @@
"authors": [
{
"name": "Sebastian Bergmann",
"role": "lead",
"email": "sebastian@phpunit.de"
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "FinderFacade is a convenience wrapper for Symfony's Finder component.",
@@ -13622,8 +13744,8 @@
"authors": [
{
"name": "Sebastian Bergmann",
"role": "lead",
"email": "sebastian@phpunit.de"
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "Collection of value objects that represent the types of the PHP type system",
@@ -13665,8 +13787,8 @@
"authors": [
{
"name": "Sebastian Bergmann",
"role": "lead",
"email": "sebastian@phpunit.de"
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "Library that helps with managing the version number of Git-hosted PHP projects",
@@ -13957,8 +14079,8 @@
"authors": [
{
"name": "Arne Blankerts",
"role": "lead",
"email": "arne@blankerts.de"
"email": "arne@blankerts.de",
"role": "lead"
}
],
"description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.",
@@ -13998,8 +14120,8 @@
"authors": [
{
"name": "Arne Blankerts",
"role": "Developer",
"email": "arne@blankerts.de"
"email": "arne@blankerts.de",
"role": "Developer"
}
],
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+82
View File
@@ -0,0 +1,82 @@
<?php
/*
* Set specific configuration variables here
*/
return [
/*
|--------------------------------------------------------------------------
| Image Driver
|--------------------------------------------------------------------------
| Avatar use Intervention Image library to process image.
| Meanwhile, Intervention Image supports "GD Library" and "Imagick" to process images
| internally. You may choose one of them according to your PHP
| configuration. By default PHP's "GD Library" implementation is used.
|
| Supported: "gd", "imagick"
|
*/
'driver' => 'gd',
// Initial generator class
'generator' => \Laravolt\Avatar\Generator\DefaultGenerator::class,
// Whether all characters supplied must be replaced with their closest ASCII counterparts
'ascii' => true,
// Image shape: circle or square
'shape' => 'square',
// Image width, in pixel
'width' => 150,
// Image height, in pixel
'height' => 150,
// Number of characters used as initials. If name consists of single word, the first N character will be used
'chars' => 2,
// font size
'fontSize' => 48,
// convert initial letter in uppercase
'uppercase' => false,
// Fonts used to render text.
// If contains more than one fonts, randomly selected based on name supplied
'fonts' => [__DIR__.'/../../vendor/laravolt/avatar/fonts/OpenSans-Bold.ttf'],
// List of foreground colors to be used, randomly selected based on name supplied
'foregrounds' => [
'#FFFFFF',
],
// List of background colors to be used, randomly selected based on name supplied
'backgrounds' => [
'#f44336',
'#E91E63',
'#9C27B0',
'#673AB7',
'#3F51B5',
'#2196F3',
'#03A9F4',
'#00BCD4',
'#009688',
'#4CAF50',
'#8BC34A',
'#CDDC39',
'#FFC107',
'#FF9800',
'#FF5722',
],
'border' => [
'size' => 1,
// border color, available value are:
// 'foreground' (same as foreground color)
// 'background' (same as background color)
// or any valid hex ('#aabbcc')
'color' => 'foreground',
],
];
+20 -11
View File
@@ -140,7 +140,7 @@ return [
|
| Available Settings: true, false
|
*/
*/
'requires_subscription' => env('REQUIRES_SUBSCRIPTION', false),
/*
@@ -153,7 +153,7 @@ return [
| the `unlock_paid_features` above.
|
|
*/
*/
'paid_plan_monthly_friendly_name' => env('PAID_PLAN_MONTHLY_FRIENDLY_NAME', null),
'paid_plan_monthly_id' => env('PAID_PLAN_MONTHLY_ID', null),
'paid_plan_monthly_price' => env('PAID_PLAN_MONTHLY_PRICE', null),
@@ -168,7 +168,7 @@ return [
|
| This value determines the number of contacts allowed on a free account.
|
*/
*/
'number_of_allowed_contacts_free_account' => env('NUMBER_OF_ALLOWED_CONTACTS_FREE_ACCOUNT', 10),
/*
@@ -179,7 +179,7 @@ return [
| This value will be the email address used in the footer of the application
| to contact support.
|
*/
*/
'support_email_address' => env('SUPPORT_EMAIL_ADDRESS', 'support@monicahq.com'),
/*
@@ -190,7 +190,7 @@ return [
| This value determines the twitter account shown in case of maintenance in
| progress.
|
*/
*/
'twitter_account' => env('SUPPORT_TWITTER', 'monicaHQ_app'),
/*
@@ -200,7 +200,7 @@ return [
|
| This value determines the maximum size when uploading a file, in kilobytes.
|
*/
*/
'max_upload_size' => env('DEFAULT_MAX_UPLOAD_SIZE', 10240),
/*
@@ -210,7 +210,7 @@ return [
|
| This the default limit for each new account. Default value: 512Mb.
|
*/
*/
'max_storage_size' => env('DEFAULT_MAX_STORAGE_SIZE', 512),
/*
@@ -223,7 +223,7 @@ return [
| If you do enable geolocation, you also need to provide a geolocation
| api key as shown below.
|
*/
*/
'enable_geolocation' => env('ENABLE_GEOLOCATION', false),
/*
@@ -236,7 +236,7 @@ return [
| want to give anything to Google, ever.
| LocationIQ offers 10,000 free requests per day.
|
*/
*/
'location_iq_api_key' => env('LOCATION_IQ_API_KEY', null),
/*
@@ -246,7 +246,7 @@ return [
|
| Geolocation needs to be enabled for this feature to work. We need to it
| to translate addresses to long/latitude coordinates.
*/
*/
'enable_weather' => env('ENABLE_WEATHER', false),
/*
@@ -257,6 +257,15 @@ return [
| To provide weather information, we use Darksky.
| Darksky provides an api with 1000 free API calls per day.
| https://darksky.net/dev/register
*/
*/
'darksky_api_key' => env('DARKSKY_API_KEY', null),
/*
|--------------------------------------------------------------------------
| Default avatar size
|--------------------------------------------------------------------------
|
| The default avatar size.
*/
'avatar_size' => 200,
];
+2
View File
@@ -127,8 +127,10 @@ $factory->define(App\Models\Contact\Contact::class, function (Faker\Generator $f
])->id;
},
'uuid' => Str::uuid(),
'default_avatar_color' => '#ffffff',
];
});
$factory->state(App\Models\Contact\Contact::class, 'partial', [
'is_partial' => 1,
]);
@@ -0,0 +1,42 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class ChangeAvatarsStructure extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('contacts', function (Blueprint $table) {
$table->string('avatar_source')->after('food_preferences')->default('default');
$table->string('avatar_gravatar_url', 250)->after('avatar_source')->nullable();
$table->uuid('avatar_adorable_uuid')->after('avatar_gravatar_url')->nullable();
$table->string('avatar_adorable_url', 250)->after('avatar_adorable_uuid')->nullable();
$table->string('avatar_default_url', 250)->after('avatar_adorable_url')->nullable();
$table->integer('avatar_photo_id')->after('avatar_default_url')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('contacts', function (Blueprint $table) {
$table->dropColumn('avatar_source');
$table->dropColumn('avatar_gravatar_url');
$table->dropColumn('avatar_adorable_uuid');
$table->dropColumn('avatar_adorable_url');
$table->dropColumn('avatar_default_url');
$table->dropColumn('avatar_photo_id');
});
}
}
@@ -0,0 +1,32 @@
<?php
use App\Models\Contact\Contact;
use Illuminate\Database\Migrations\Migration;
use App\Services\Contact\Avatar\GenerateDefaultAvatar;
use App\Services\Contact\Avatar\GetAvatarsFromInternet;
/**
* This creates all the avatars (default, adorable and gravatars) for existing
* contacts.
*/
class CreateAvatarsForExistingContacts extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Contact::chunk(200, function ($contacts) {
foreach ($contacts as $contact) {
$request = [
'contact_id' => $contact->id,
];
app(GetAvatarsFromInternet::class)->execute($request);
app(GenerateDefaultAvatar::class)->execute($request);
}
});
}
}
-15
View File
@@ -82,17 +82,6 @@ class FakeContentTableSeeder extends Seeder
'is_deceased_date_known' => false,
]);
$this->contact->setAvatarColor();
$this->contact->save();
// set an external avatar
if (rand(1, 2) == 1) {
$this->contact->has_avatar = true;
$this->contact->avatar_location = 'external';
$this->contact->avatar_external_url = $arrayPictures->results[$i]->picture->large;
$this->contact->save();
}
$this->populateTags();
$this->populateFoodPreferences();
$this->populateDeceasedDate();
@@ -249,10 +238,6 @@ class FakeContentTableSeeder extends Seeder
'is_deceased_date_known' => false,
]);
$relatedContact->setAvatarColor();
$relatedContact->save();
// birthdate
$relatedContactBirthDate = $this->faker->dateTimeThisCentury();
app(UpdateBirthdayInformation::class)->execute([
'account_id' => $this->contact->account_id,
+1
View File
@@ -25,6 +25,7 @@ parameters:
- '#Call to an undefined method Illuminate\\Database\\Eloquent\\Relations\\HasMany::completed\(\)\.#'
- '#Call to an undefined method Illuminate\\Database\\Eloquent\\Relations\\HasMany::email\(\)\.#'
- '#Call to an undefined method Illuminate\\Database\\Eloquent\\Relations\\HasMany::phone\(\)\.#'
- '#Parameter \#2 \$configGroup of static method Creativeorange\\Gravatar\\Gravatar::get\(\) expects string, array<string, bool\|int\|string> given\.#'
# Level 3
- '#Method [a-zA-Z0-9\\_:]+\(\) should return [a-zA-Z0-9\\_\|]+ but empty return statement found\.#'
- '#Method [a-zA-Z0-9\\_:]+\(\) should return Illuminate\\Http\\Response but returns [a-zA-Z0-9\\_]+\.#'
+5
View File
@@ -1,5 +1,10 @@
{
"entries": [
{
"date": "Aug 17, 2019",
"title": "New feature: you can now change avatars",
"description": "You can now change the avatar of your contacts. Mouse over the profile picture and have fun. ![image](/img/changelogs/2018-12-16-avatars.png)"
},
{
"date": "May 04, 2019",
"title": "New feature: WebAuthn two factor authentication",
+2 -2
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -1,9 +1,9 @@
{
"/js/manifest.js": "/js/manifest.js?id=01c8731923a46c30aaed",
"/js/vendor.js": "/js/vendor.js?id=4638be4cb4d81526ae20",
"/js/app.js": "/js/app.js?id=808634454b20b3ea14e5",
"/css/app-ltr.css": "/css/app-ltr.css?id=d1bc921f46ec60c3bbca",
"/css/app-rtl.css": "/css/app-rtl.css?id=72920cbd345b2fe95fd4",
"/js/vendor.js": "/js/vendor.js?id=bad0e07ad4bf8abb5b2a",
"/js/app.js": "/js/app.js?id=455c179a58dd43584956",
"/css/app-ltr.css": "/css/app-ltr.css?id=2f2e6d6ecec7b4427d4a",
"/css/app-rtl.css": "/css/app-rtl.css?id=57de47acc713e3eb5cf8",
"/css/stripe.css": "/css/stripe.css?id=746c8aaac01c56d3cee1",
"/js/stripe.js": "/js/stripe.js?id=95472edcb28fd5bd5d9c",
"/js/u2f-api.js": "/js/u2f-api.js?id=1948d3efdfd801bed14a"
+10 -1
View File
@@ -122,6 +122,10 @@ Vue.component(
require('./components/people/Tags.vue').default
);
Vue.component(
'contact-avatar',
require('./components/people/SetAvatar.vue').default
);
Vue.component(
'contact-favorite',
require('./components/people/SetFavorite.vue').default
@@ -316,7 +320,12 @@ common.loadLanguage(window.Laravel.locale, true).then((i18n) => {
.then(response => {
this.global_profile_default_view = view;
});
}
},
fixAvatarDisplay(event) {
event.srcElement.classList = ['hidden'];
event.srcElement.nextElementSibling.classList.remove('hidden');
},
}
}).$mount('#app');
@@ -93,11 +93,7 @@
{{ $t('app.with') }}
</span>
<div v-for="attendees in activity.attendees" :key="attendees.id" class="dib pointer ml2" @click="redirect(attendees)">
<img v-if="attendees.information.avatar.has_avatar" v-tooltip="attendees.complete_name" :src="attendees.information.avatar.avatar_url" class="br3 journal-avatar-small" />
<img v-else-if="attendees.information.avatar.gravatar_url" v-tooltip.bottom="attendees.complete_name" :src="attendees.information.avatar.gravatar_url" class="br3 journal-avatar-small" />
<div v-else v-tooltip.bottom="attendees.complete_name" :style="{ 'background-color': attendees.information.avatar.default_avatar_color }" class="br3 white tc journal-initial-small">
{{ attendees.initials }}
</div>
<img v-tooltip.bottom="attendees.complete_name" :src="attendees.information.avatar.avatar_url" class="br3 journal-avatar-small" />
</div>
</div>
</div>
@@ -5,15 +5,35 @@
<div>
<template v-if="clickable == true">
<a :href="'people/' + contact.id">
<img v-if="contact.avatar_url" v-tooltip.bottom="contact.complete_name" :src="contact.avatar_url" class="br4 h3 w3 dib tc" />
<div v-else v-tooltip.bottom="contact.complete_name" :style="{ 'background-color': contact.default_avatar_color }" class="br4 h3 w3 dib pt3 white tc f4">
<img v-if="check"
v-tooltip.bottom="contact.complete_name"
class="br4 h3 w3 dib tc"
:alt="contact.initials"
:src="contact.avatar_url"
@error="check=false"
/>
<div v-else
v-tooltip.bottom="contact.complete_name"
class="br4 h3 w3 dib tc white f3"
:style="'padding-top: 12px; background-color: '+ contact.default_avatar_color"
>
{{ contact.initials }}
</div>
</a>
</template>
<template v-else>
<img v-if="contact.avatar_url" v-tooltip.bottom="contact.complete_name" :src="contact.avatar_url" class="br4 h3 w3 dib tc" />
<div v-else v-tooltip.bottom="contact.complete_name" :style="{ 'background-color': contact.default_avatar_color }" class="br4 h3 w3 dib pt3 white tc f4">
<img v-if="check"
v-tooltip.bottom="contact.complete_name"
class="br4 h3 w3 dib tc"
:alt="contact.initials"
:src="contact.avatar_url"
@error="check=false"
/>
<div v-else
v-tooltip.bottom="contact.complete_name"
class="br4 h3 w3 dib tc white f3"
:style="'padding-top: 12px; background-color: '+ contact.default_avatar_color"
>
{{ contact.initials }}
</div>
</template>
@@ -32,5 +52,10 @@ export default {
default: true,
},
},
data () {
return {
check: true,
};
}
};
</script>
@@ -19,7 +19,7 @@
</p-input>
</div>
<div class="pointer" @click="select()">
<label v-if="hasSlot('label')" class="pointer" :for="formid">
<label v-if="hasSlot('label')" class="pointer">
<slot name="label"></slot>
</label>
<slot name="extra"></slot>
@@ -88,10 +88,10 @@ export default {
},
methods: {
formid() {
return this.$refs.input.id;
},
select() {
if (this.disabled) {
return;
}
switch (this._type)
{
case 'checkbox':
@@ -0,0 +1,152 @@
<style scoped>
</style>
<template>
<div class="pa4-ns ph3 pv2 bb b--gray-monica">
<p>{{ $t('people.avatar_question') }}</p>
<div class="mb3 mb0-ns">
<!-- Default avatar -->
<form-radio
v-model="avatar"
:name="'avatar'"
:value="'default'"
:dclass="'flex mb1'"
:iclass="dirltr ? 'mr2' : 'ml2'"
>
<template slot="label">
{{ $t('people.avatar_default_avatar') }}
</template>
<div slot="extra">
<img class="mb4 pa2 ba b--gray-monica br3" style="width: 150px" :src="defaultUrl" alt="" />
</div>
</form-radio>
<!-- Adorable avatar -->
<form-radio
v-model="avatar"
:name="'avatar'"
:value="'adorable'"
:dclass="'flex mb1'"
:iclass="dirltr ? 'mr2' : 'ml2'"
>
<template slot="label">
{{ $t('people.avatar_adorable_avatar') }}
</template>
<div slot="extra">
<img class="mb4 pa2 ba b--gray-monica br3" style="width: 150px" :src="adorableUrl" alt="" />
</div>
</form-radio>
<!-- Gravatar -->
<form-radio
v-if="gravatarUrl"
v-model="avatar"
:name="'avatar'"
:value="'gravatar'"
:dclass="'flex mb1'"
:iclass="dirltr ? 'mr2' : 'ml2'"
>
<template slot="label">
<span v-html="$t('people.avatar_gravatar')"></span>
</template>
<div slot="extra">
<img class="mb4 pa2 ba b--gray-monica br3" style="width: 150px" :src="gravatarUrl" alt="" />
</div>
</form-radio>
<!-- Existing avatar -->
<form-radio
v-if="initialAvatar === 'photo'"
v-model="avatar"
:name="'avatar'"
:value="'photo'"
:dclass="'flex mb1'"
:iclass="dirltr ? 'mr2' : 'ml2'"
>
<template slot="label">
{{ $t('people.avatar_current') }}
</template>
<div slot="extra">
<img class="mb4 pa2 ba b--gray-monica br3" style="width: 150px" :src="photoUrl" alt="" />
</div>
</form-radio>
<!-- Upload avatar -->
<form-radio
v-model="avatar"
:name="'avatar'"
:value="'upload'"
:dclass="'flex mb1'"
:iclass="dirltr ? 'mr2' : 'ml2'"
:disabled="hasReachedAccountStorageLimit"
>
<template slot="label">
{{ $t('people.avatar_photo') }}
<span v-if="hasReachedAccountStorageLimit">
<a href="/settings/subscriptions">
{{ $t('app.upgrade') }}
</a>
</span>
</template>
<div slot="extra">
<input type="file" class="form-control-file" name="photo" :disabled="hasReachedAccountStorageLimit" />
<small class="form-text text-muted">
{{ $t('people.information_edit_max_size2', { size: maxUploadSize }) }}
</small>
</div>
</form-radio>
</div>
</div>
</template>
<script>
export default {
props: {
avatar: {
type: String,
default: '',
},
defaultUrl: {
type: String,
default: '',
},
adorableUrl: {
type: String,
default: '',
},
gravatarUrl: {
type: String,
default: '',
},
photoUrl: {
type: String,
default: '',
},
hasReachedAccountStorageLimit: {
type: Boolean,
default: false,
},
maxUploadSize: {
type: Number,
default: 10000,
},
},
data() {
return {
initialAvatar: '',
};
},
computed: {
dirltr() {
return this.$root.htmldir == 'ltr';
}
},
mounted() {
this.initialAvatar = this.avatar;
},
};
</script>
@@ -53,9 +53,10 @@
<template>
<div v-if="item.id > 0" class="item-search-result" :data-contact="item.id" :data-name="item.name">
<a :href="'/people/'+item.hash_id">
<img v-if="item.information.avatar.has_avatar"
:src="item.information.avatar.avatar_url"
<img v-if="check"
class="avatar"
:src="item.information.avatar.url"
@error="check=false"
/>
<div v-else
class="avatar avatar-initials"
@@ -84,6 +85,11 @@ export default {
required: true,
default: null,
},
}
},
data() {
return {
check: true,
};
},
};
</script>
@@ -52,9 +52,10 @@
<template>
<div v-if="item.id > 0" class="item-search-result" :data-contact="item.id" :data-name="item.name">
<div class="item-search-result-result pointer">
<img v-if="item.information.avatar.has_avatar"
:src="item.information.avatar.avatar_url"
<img v-if="check"
class="avatar"
:src="item.information.avatar.url"
@error="check=false"
/>
<div v-else
class="avatar avatar-initials"
@@ -63,6 +64,7 @@
{{ item.initials }}
</div>
{{ item.complete_name }}
<span></span>
</div>
</div>
<div v-else class="item-search-result">
@@ -84,6 +86,11 @@ export default {
required: true,
default: null,
},
}
},
data() {
return {
check: true,
};
},
};
</script>
@@ -2,28 +2,33 @@
.photo {
height: 250px;
}
.photo-upload-zone {
.photo-upload-zone {
background: #fff;
border: 1px solid #d6d6d6;
border-style: dashed;
}
progress {
progress {
-webkit-appearance: none;
border: none;
height: 8px;
margin-bottom: 3px;
width: 60%;
}
progress::-webkit-progress-bar {
progress::-webkit-progress-bar {
background: #e2e7ee;
border-radius: 3px;
}
progress::-webkit-progress-value {
progress::-webkit-progress-value {
border-radius: 3px;
box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.4);
background-color: #329FF1;
}
</style>
<template>
<div>
<div class="">
@@ -39,9 +44,11 @@
</span>
</h3>
</div>
<p v-show="reachLimit == 'true'">
{{ $t('settings.storage_upgrade_notice') }}
</p>
<!-- EMPTY STATE -->
<div v-if="displayUploadZone == false && displayUploadError == false && displayUploadProgress == false && photos.length == 0" class="ltr w-100 pt2">
<div class="section-blank">
@@ -51,6 +58,7 @@
<img src="img/people/photos/photos_empty.svg" class="w-50 center" />
</div>
</div>
<!-- FIRST STEP OF PHOTO UPLOAD -->
<transition name="fade">
<div v-if="displayUploadZone" class="ba br3 photo-upload-zone mb3 pa3">
@@ -66,6 +74,7 @@
</div>
</div>
</transition>
<!-- LAST STEP OF PHOTO UPLOAD -->
<div v-if="displayUploadProgress" class="ba br3 photo-upload-zone mb3 pa3">
<p class="tc mb1">
@@ -78,6 +87,7 @@
{{ $t('app.percent_uploaded', {percent: uploadPercentage}) }}
</p>
</div>
<!-- ERROR STEP WHEN UPLOADING A PHOTO -->
<transition name="fade">
<div v-if="displayUploadError" class="ba br3 photo-upload-zone mb3 pa3">
@@ -98,6 +108,7 @@
</p>
</div>
</transition>
<!-- LIST OF PHOTO -->
<div class="db mt3">
<div class="flex flex-wrap">
@@ -107,6 +118,14 @@
</div>
<div class="pt2">
<ul>
<li v-show="currentPhotoIdAsAvatar == photo.id">
🤩 {{ $t('people.photo_current_profile_pic') }}
</li>
<li v-show="currentPhotoIdAsAvatar != photo.id">
<a class="pointer" @click.prevent="makeProfilePicture(photo)">
{{ $t('people.photo_make_profile_pic') }}
</a>
</li>
<li v-show="confirmDestroyPhotoId != photo.id">
<a class="pointer" href="" @click.prevent="confirmDestroyPhotoId = photo.id">
{{ $t('people.photo_delete') }}
@@ -125,6 +144,7 @@
</div>
</div>
</div>
<!-- MODAL ZOOM PHOTO -->
<transition v-if="showModal" name="modal">
<div class="modal-mask">
@@ -160,7 +180,7 @@ export default {
default: '',
},
},
data() {
return {
photos: [],
@@ -261,4 +281,4 @@ export default {
}
}
};
</script>
</script>
+3 -2
View File
@@ -33,10 +33,10 @@ $('a[href^="#logCallModal"]').click(function (e) {
});
// On the contact sheet of a person, for the Deceased section
$('#is_deceased').click(function () {
$('#is_deceased').click(function() {
$('#datePersonDeceased').toggle(this.checked);
if (!document.getElementById('markPersonDeceased').checked) {
if(! document.getElementById('markPersonDeceased').checked) {
$('#is_deceased_date_known').prop('checked', false);
$('#add_reminder_deceased').prop('checked', false);
$('#datesSelector').prop('checked', false);
@@ -45,6 +45,7 @@ $('#is_deceased').click(function () {
}
});
$('#is_deceased_date_known').click(function () {
$('#reminderDeceased').toggle(this.checked);
$('#datesSelector').toggle(this.checked);
+27 -28
View File
@@ -126,34 +126,6 @@
}
}
.avatar {
background-color: #93521e;
border-radius: 3px;
color: #fff;
display: inline-block;
font-size: 15px;
height: 43px;
margin-right: 5px;
padding-top: 10px;
vertical-align: middle;
width: 43px;
@if $htmldir == ltr {
padding-left: 5px;
} @else {
padding-right: 5px;
}
&.one-letter {
padding-left: 0;
text-align: center;
}
}
img {
border-radius: 3px;
margin-right: 5px;
}
a {
color: #333;
text-decoration: none;
@@ -206,6 +178,33 @@
}
}
.avatar-header {
top: 40px;
margin-top: -30px;
.image-header::after {
box-shadow: inset 1px 1px 3px 0 rgba(173,173,173,0.50);
border-radius: 7px;
content: '';
display: block;
height: 100%;
position: absolute;
top: 0;
width: 100%;
}
.hide-child:hover {
.child {
background: rgba(90, 90, 90, 0.96);
}
a:hover {
background: transparent;
text-decoration: underline;
}
}
}
.people-show {
.main-content {
background-color: #fff;
+12 -1
View File
@@ -135,8 +135,8 @@ return [
// additional information
'information_edit_success' => 'The profile has been updated successfully',
'information_edit_title' => 'Edit :names personal information',
'information_edit_avatar' => 'Photo/avatar of the contact',
'information_edit_max_size' => 'Max :size Kb.',
'information_edit_max_size2' => 'Max {size} Kb.',
'information_edit_firstname' => 'First name',
'information_edit_lastname' => 'Last name (Optional)',
'information_edit_description' => 'Description (Optional)',
@@ -463,8 +463,19 @@ return [
'photo_list_cta' => 'Upload photo',
'photo_list_blank_desc' => 'You can store images about this contact. Upload one now!',
'photo_upload_zone_cta' => 'Upload a photo',
'photo_current_profile_pic' => 'Current profile picture',
'photo_make_profile_pic' => 'Make profile picture',
'photo_delete' => 'Delete photo',
// Avatars
'avatar_change_title' => 'Change your avatar',
'avatar_question' => 'Which avatar would you like to use?',
'avatar_default_avatar' => 'The default avatar',
'avatar_adorable_avatar' => 'The Adorable avatar',
'avatar_gravatar' => 'The Gravatar associated with the email address of this person. <a href="https://gravatar.com/">Gravatar</a> is a global system that lets users associate email addresses with photos.',
'avatar_current' => 'Keep the current avatar',
'avatar_photo' => 'From a photo that you upload',
// emotions
'emotion_this_made_me_feel' => 'This made you feel…',
];
+29 -6
View File
@@ -6,14 +6,37 @@
</div>
@endif
<div class="mw9 center tc w-100 avatar-header relative">
{{-- AVATAR --}}
<div class="relative center dib z-3">
<div class="relative hide-child">
<div class="image-header top-0 left-0">
<img class="cover br3 bb b--gray-monica"
alt={{ $contact->initials }}
src="{{ $contact->getAvatarURL() }}"
style="height: 115px; width: 115px;"
v-on:error="fixAvatarDisplay"
/>
<div class="hidden dib white tc f1"
style="padding-top: 21px; height: 115px; width: 115px; background-color: {{ $contact->default_avatar_color }}"
>
{{ $contact->initials }}
</div>
</div>
<div class="child absolute top-0 left-0 h-100 w-100 br3">
<div class="db w-100 h-100 center tc pt5">
<a class="no-underline white" href="{{ route('people.avatar.edit', $contact) }}">
📷 {{ trans('app.update' )}}
</a>
</div>
</div>
</div>
</div>
</div>
<div class="mw9 center dt w-100 box-shadow pa4 relative">
{{-- AVATAR --}}
<div class="relative">
{!! $avatar !!}
</div>
<h1 class="tc mb2 mt0">
<h1 class="tc mb2 mt4">
<span class="{{ htmldir() == 'ltr' ? 'mr1' : 'ml1' }}">{{ $contact->name }}</span>
<contact-favorite hash="{{ $contact->hashID() }}" :starred="{{ \Safe\json_encode($contact->is_starred) }}"></contact-favorite>
</h1>
@@ -0,0 +1,44 @@
@extends('layouts.skeleton')
@section('content')
<section class="ph3 ph0-ns">
{{-- Breadcrumb --}}
<div class="mt4 mw7 center mb3">
<p><a href="{{ route('people.show', $contact) }}">< {{ $contact->name }}</a></p>
<h3 class="f3 fw5">{{ trans('people.avatar_change_title') }}</h3>
</div>
<div class="mw7 center br3 ba b--gray-monica bg-white mb5">
<form method="POST" action="{{ route('people.avatar.update', $contact) }}" enctype="multipart/form-data">
{{ csrf_field() }}
@include('partials.errors')
{{-- Adorable --}}
<contact-avatar
:avatar="'{{ $contact->avatar_source }}'"
:default-url="'{{ $contact->getAvatarDefaultURL() }}'"
:adorable-url="'{{ $contact->avatar_adorable_url }}'"
:gravatar-url="'{{ $contact->avatar_gravatar_url }}'"
:photo-url="'{{ $contact->getAvatarURL() }}'"
:has-reached-account-storage-limit="false"
:max-upload-size="{{ config('monica.max_upload_size') }}"
>
</contact-avatar>
{{-- Form actions --}}
<div class="ph4-ns ph3 pv3 bb b--gray-monica">
<div class="flex-ns justify-between">
<div>
<a href="{{ route('people.show', $contact) }}" class="btn btn-secondary w-auto-ns w-100 mb2 pb0-ns">{{ trans('app.cancel') }}</a>
</div>
<div>
<button class="btn btn-primary w-auto-ns w-100 mb2 pb0-ns" name="save" type="submit">{{ trans('app.save') }}</button>
</div>
</div>
</div>
</form>
</div>
</div>
@endsection
-9
View File
@@ -119,15 +119,6 @@
</div>
</div>
{{-- Avatar --}}
<div class="pa4-ns ph3 pv2 bb b--gray-monica">
<div class="mb3 mb0-ns">
<label for="avatar">{{ trans('people.information_edit_avatar') }}</label>
<input type="file" class="form-control-file" name="avatar" id="avatar">
<small id="fileHelp" class="form-text text-muted">{{ trans('people.information_edit_max_size', ['size' => config('monica.max_upload_size')]) }}</small>
</div>
</div>
{{-- Birthdate --}}
<form-specialdate
:months="{{ $months }}"
+28 -41
View File
@@ -108,31 +108,25 @@
<li class="people-list-item bg-white pointer"
@click="window.location.href='{{ route('people.show', $contact) }}'">
<a href="{{ route('people.show', $contact) }}">
@if ($contact->has_avatar)
<img src="{{ $contact->getAvatarURL(110) }}" width="43">
@else
@if (! is_null($contact->gravatar_url))
<img src="{{ $contact->gravatar_url }}" width="43">
@else
@if (strlen($contact->getInitials()) == 1)
<div class="avatar one-letter" style="background-color: {{ $contact->getAvatarColor() }};">
{{ $contact->getInitials() }}
</div>
@else
<div class="avatar" style="background-color: {{ $contact->getAvatarColor() }};">
{{ $contact->getInitials() }}
</div>
@endif
@endif
@endif
<img class="br4 h3 w3 dib tc"
alt={{ $contact->initials }}
src="{{ $contact->getAvatarURL() }}"
v-on:error="(e) => { fixAvatarDisplay(e); }"
/>
<div class="hidden br4 h3 w3 dib tc white f3"
style="padding-top: 12px; background-color: {{ $contact->default_avatar_color }}"
>
{{ $contact->initials }}
</div>
<span>
<span class="{{ htmldir() == 'ltr' ? 'mr1' : 'ml1' }}">{{ $contact->name }}</span>
<svg class="relative" style="top: 5px" width="23" height="22" viewBox="0 0 23 22" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M14.6053 7.404L14.7224 7.68675L15.0275 7.7111C16.7206 7.84628 18.1486 7.92359 19.2895 7.98536C19.9026 8.01855 20.4327 8.04725 20.8765 8.07803C21.5288 8.12327 21.9886 8.17235 22.2913 8.24003C22.3371 8.25027 22.3765 8.26035 22.4102 8.27001C22.3896 8.29619 22.3651 8.32572 22.336 8.35877C22.1328 8.58945 21.7914 8.89714 21.2913 9.31475C20.9474 9.60184 20.5299 9.93955 20.0459 10.331C19.1625 11.0455 18.0577 11.9391 16.7762 13.0302L16.543 13.2288L16.614 13.5267C17.0045 15.1663 17.3689 16.5406 17.6601 17.6391C17.819 18.2381 17.956 18.7552 18.0638 19.1886C18.2209 19.8206 18.3149 20.2704 18.3428 20.5768C18.347 20.6222 18.3493 20.6616 18.3505 20.6957C18.3176 20.6838 18.2798 20.6688 18.2367 20.6502C17.9532 20.5277 17.5539 20.2981 17.0014 19.9522C16.6264 19.7175 16.1833 19.4306 15.671 19.099C14.7143 18.4797 13.5162 17.7042 12.0695 16.8199L11.8087 16.6604L11.5478 16.82C10.0753 17.7209 8.86032 18.5085 7.89223 19.136C7.3851 19.4648 6.94572 19.7496 6.57253 19.9838C6.01576 20.3332 5.61353 20.5656 5.32808 20.6899C5.28721 20.7077 5.25111 20.7222 5.21941 20.7339C5.22088 20.7009 5.22355 20.663 5.22783 20.6197C5.25839 20.3111 5.35605 19.8582 5.51781 19.2225C5.62627 18.7962 5.76269 18.2914 5.92018 17.7087C6.22053 16.5972 6.59748 15.2024 7.00309 13.5286L7.07553 13.2297L6.84141 13.0303C5.52399 11.9079 4.39683 10.9982 3.50024 10.2747C3.03915 9.90254 2.63904 9.57963 2.30539 9.30232C1.80195 8.88388 1.45729 8.57562 1.25116 8.34437C1.22315 8.31293 1.19929 8.28466 1.17903 8.25939C1.20999 8.25084 1.24557 8.24198 1.28628 8.233C1.58841 8.1663 2.048 8.11835 2.701 8.07418C3.1353 8.0448 3.65101 8.01744 4.24568 7.98589C5.39523 7.9249 6.83989 7.84824 8.56208 7.71111L8.86638 7.68688L8.98388 7.40514C9.61646 5.88824 10.1238 4.58366 10.5314 3.53571C10.7656 2.93365 10.9668 2.4163 11.1399 1.99205C11.3854 1.39027 11.5751 0.972355 11.7339 0.708729C11.7601 0.66516 11.7838 0.628777 11.8048 0.598565C11.8256 0.628571 11.849 0.664658 11.8748 0.707817C12.0327 0.971308 12.2212 1.38911 12.465 1.99089C12.6368 2.41509 12.8365 2.93242 13.0689 3.53445C13.4735 4.58244 13.9771 5.88709 14.6053 7.404Z"
fill="#F2C94C" stroke="#DCBB58"/>
</svg>
</span>
<span class="{{ htmldir() == 'ltr' ? 'mr1' : 'ml1' }}">{{ $contact->name }}</span>
<svg class="relative" style="top: 5px" width="23" height="22" viewBox="0 0 23 22" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M14.6053 7.404L14.7224 7.68675L15.0275 7.7111C16.7206 7.84628 18.1486 7.92359 19.2895 7.98536C19.9026 8.01855 20.4327 8.04725 20.8765 8.07803C21.5288 8.12327 21.9886 8.17235 22.2913 8.24003C22.3371 8.25027 22.3765 8.26035 22.4102 8.27001C22.3896 8.29619 22.3651 8.32572 22.336 8.35877C22.1328 8.58945 21.7914 8.89714 21.2913 9.31475C20.9474 9.60184 20.5299 9.93955 20.0459 10.331C19.1625 11.0455 18.0577 11.9391 16.7762 13.0302L16.543 13.2288L16.614 13.5267C17.0045 15.1663 17.3689 16.5406 17.6601 17.6391C17.819 18.2381 17.956 18.7552 18.0638 19.1886C18.2209 19.8206 18.3149 20.2704 18.3428 20.5768C18.347 20.6222 18.3493 20.6616 18.3505 20.6957C18.3176 20.6838 18.2798 20.6688 18.2367 20.6502C17.9532 20.5277 17.5539 20.2981 17.0014 19.9522C16.6264 19.7175 16.1833 19.4306 15.671 19.099C14.7143 18.4797 13.5162 17.7042 12.0695 16.8199L11.8087 16.6604L11.5478 16.82C10.0753 17.7209 8.86032 18.5085 7.89223 19.136C7.3851 19.4648 6.94572 19.7496 6.57253 19.9838C6.01576 20.3332 5.61353 20.5656 5.32808 20.6899C5.28721 20.7077 5.25111 20.7222 5.21941 20.7339C5.22088 20.7009 5.22355 20.663 5.22783 20.6197C5.25839 20.3111 5.35605 19.8582 5.51781 19.2225C5.62627 18.7962 5.76269 18.2914 5.92018 17.7087C6.22053 16.5972 6.59748 15.2024 7.00309 13.5286L7.07553 13.2297L6.84141 13.0303C5.52399 11.9079 4.39683 10.9982 3.50024 10.2747C3.03915 9.90254 2.63904 9.57963 2.30539 9.30232C1.80195 8.88388 1.45729 8.57562 1.25116 8.34437C1.22315 8.31293 1.19929 8.28466 1.17903 8.25939C1.20999 8.25084 1.24557 8.24198 1.28628 8.233C1.58841 8.1663 2.048 8.11835 2.701 8.07418C3.1353 8.0448 3.65101 8.01744 4.24568 7.98589C5.39523 7.9249 6.83989 7.84824 8.56208 7.71111L8.86638 7.68688L8.98388 7.40514C9.61646 5.88824 10.1238 4.58366 10.5314 3.53571C10.7656 2.93365 10.9668 2.4163 11.1399 1.99205C11.3854 1.39027 11.5751 0.972355 11.7339 0.708729C11.7601 0.66516 11.7838 0.628777 11.8048 0.598565C11.8256 0.628571 11.849 0.664658 11.8748 0.707817C12.0327 0.971308 12.2212 1.38911 12.465 1.99089C12.6368 2.41509 12.8365 2.93242 13.0689 3.53445C13.4735 4.58244 13.9771 5.88709 14.6053 7.404Z"
fill="#F2C94C" stroke="#DCBB58"/>
</svg>
</span>
<span class="i {{ htmldir() == 'ltr' ? 'ml3' : 'mr3' }}">{{ $contact->description }}</span>
</a>
</li>
@@ -143,23 +137,16 @@
<li class="people-list-item bg-white pointer"
@click="window.location.href='{{ route('people.show', $contact) }}'">
<a href="{{ route('people.show', $contact) }}">
@if ($contact->has_avatar)
<img src="{{ $contact->getAvatarURL(110) }}" width="43">
@else
@if (! is_null($contact->gravatar_url))
<img src="{{ $contact->gravatar_url }}" width="43">
@else
@if (strlen($contact->getInitials()) == 1)
<div class="avatar one-letter" style="background-color: {{ $contact->getAvatarColor() }};">
{{ $contact->getInitials() }}
</div>
@else
<div class="avatar" style="background-color: {{ $contact->getAvatarColor() }};">
{{ $contact->getInitials() }}
</div>
@endif
@endif
@endif
<img class="br4 h3 w3 dib tc"
alt={{ $contact->initials }}
src="{{ $contact->getAvatarURL() }}"
v-on:error="fixAvatarDisplay"
/>
<div class="hidden br4 h3 w3 dib tc white f3"
style="padding-top: 12px; background-color: {{ $contact->default_avatar_color }}"
>
{{ $contact->initials }}
</div>
<span>
{{ $contact->name }}
</span>
@@ -7,4 +7,4 @@
reach-limit="{{ \Safe\json_encode($contact->account->hasReachedAccountStorageLimit()) }}">
</photo-list>
</div>
</div>
</div>
+5
View File
@@ -63,6 +63,11 @@ Route::middleware(['auth', 'verified', 'mfa'])->group(function () {
Route::put('/people/{contact}', 'ContactsController@update')->name('update');
Route::delete('/people/{contact}', 'ContactsController@destroy')->name('destroy');
// Avatar
Route::get('/people/{contact}/avatar', 'Contacts\\AvatarController@edit')->name('avatar.edit');
Route::post('/people/{contact}/avatar', 'Contacts\\AvatarController@update')->name('avatar.update');
Route::post('/people/{contact}/makeProfilePicture/{photo}', 'Contacts\\AvatarController@photo')->name('avatar.photo');
// Life events
Route::name('lifeevent.')->group(function () {
Route::get('/people/{contact}/lifeevents', 'Contacts\\LifeEventsController@index')->name('index');
+8
View File
@@ -25,6 +25,7 @@ trait CardEtag
protected function getCard(Contact $contact, bool $realFormat = false): string
{
$contact = $contact->refresh();
$url = route('people.show', $contact);
$sabreversion = \Sabre\VObject\Version::VERSION;
$timestamp = $contact->updated_at->format('Ymd\THis\Z');
@@ -37,10 +38,17 @@ SOURCE:{$url}
FN:{$contact->name}
N:{$contact->last_name};{$contact->first_name};{$contact->middle_name};;
";
if ($contact->gender) {
$data .= "GENDER:{$contact->gender->type}";
$data .= "\n";
}
$picture = $contact->getAvatarURL();
if (! empty($picture)) {
$data .= "PHOTO;VALUE=URI:{$picture}\n";
}
foreach ($contact->addresses as $address) {
$data .= 'ADR:;;';
$data .= $address->place->street.';';
@@ -0,0 +1,90 @@
<?php
namespace Tests\Jobs;
use Tests\TestCase;
use App\Models\User\User;
use App\Models\Account\Photo;
use App\Models\Contact\Contact;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Storage;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class MoveAvatarsToPhotosDirectoryTest extends TestCase
{
use DatabaseTransactions;
/**
* Returns an array containing a user object along with
* a contact for that user.
* @return array
*/
private function fetchUser()
{
$user = factory(User::class)->create();
$contact = factory(Contact::class)->create([
'account_id' => $user->account_id,
]);
return [$user, $contact];
}
public function test_it_move_avatars_to_photo_directory()
{
[$user, $contact] = $this->fetchUser();
Storage::fake('public');
Storage::disk('public')->put('avatars/avatar.jpg', 'content');
Storage::disk('public')->put('avatars/avatar_110.jpg', 'content');
Storage::disk('public')->put('avatars/avatar_174.jpg', 'content');
$contact->avatar_file_name = 'avatars/avatar.jpg';
$contact->avatar_location = 'public';
$contact->has_avatar = true;
$contact->save();
Storage::disk('public')->assertExists('avatars/avatar.jpg');
$exitCode = Artisan::call('monica:moveavatarstophotosdirectory');
Storage::disk('public')->assertMissing('avatars/avatar.jpg');
Storage::disk('public')->assertMissing('avatars/avatar_110.jpg');
Storage::disk('public')->assertMissing('avatars/avatar_174.jpg');
$contact->refresh();
$photo = Photo::find($contact->avatar_photo_id);
$this->assertDatabaseHas('contacts', [
'id' => $contact->id,
'avatar_source' => 'photo',
]);
$this->assertStringContainsString('photos/', $photo->new_filename);
Storage::disk('public')->assertExists($photo->new_filename);
}
public function test_it_handles_missing_avatar()
{
[$user, $contact] = $this->fetchUser();
Storage::fake('public');
$contact->avatar_file_name = 'avatars/avatar.jpg';
$contact->avatar_location = 'public';
$contact->has_avatar = true;
$contact->save();
$exitCode = Artisan::call('monica:moveavatarstophotosdirectory');
Storage::disk('public')->assertMissing('avatars/avatar.jpg');
$this->assertDatabaseHas('contacts', [
'id' => $contact->id,
'avatar_source' => 'default',
'avatar_file_name' => 'avatars/avatar.jpg',
'avatar_location' => 'public',
]);
}
}
+136
View File
@@ -0,0 +1,136 @@
<?php
namespace Tests\Feature;
use Tests\FeatureTestCase;
use App\Models\Account\Photo;
use App\Models\Contact\Contact;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class AvatarTest extends FeatureTestCase
{
use DatabaseTransactions;
/**
* Returns an array containing a user object along with
* a contact for that user.
* @return array
*/
private function fetchUser()
{
$user = $this->signIn();
$contact = factory(Contact::class)->create([
'account_id' => $user->account_id,
]);
return [$user, $contact];
}
public function test_user_can_add_an_avatar_as_photo()
{
[$user, $contact] = $this->fetchUser();
Storage::fake('public');
$file = UploadedFile::fake()->image('avatar.jpg');
$params = [
'avatar' => 'upload',
'photo' => $file,
];
$response = $this->post('/people/'.$contact->hashID().'/avatar', $params);
$response->assertStatus(302);
// Assert the photo has been added for the correct user.
$this->assertDatabaseHas('photos', [
'account_id' => $user->account_id,
'original_filename' => 'avatar.jpg',
'new_filename' => 'photos/'.$file->hashName(),
]);
$this->assertDatabaseHas('contacts', [
'id' => $contact->id,
'account_id' => $user->account_id,
'avatar_source' => 'photo',
]);
$this->assertDatabaseHas('contact_photo', [
'contact_id' => $contact->id,
]);
Storage::disk('public')->assertExists('photos/'.$file->hashName());
}
public function test_user_can_add_an_avatar_as_adorable()
{
[$user, $contact] = $this->fetchUser();
$params = [
'avatar' => 'adorable',
];
$response = $this->post('/people/'.$contact->hashID().'/avatar', $params);
$response->assertStatus(302);
$this->assertDatabaseHas('contacts', [
'id' => $contact->id,
'account_id' => $user->account_id,
'avatar_source' => 'adorable',
]);
}
public function test_user_can_associate_an_avatar_as_photo()
{
[$user, $contact] = $this->fetchUser();
Storage::fake('public');
$file = UploadedFile::fake()->image('avatar.jpg');
$params = [
'avatar' => 'upload',
'photo' => $file,
];
$response = $this->post('/people/'.$contact->hashID().'/avatar', $params);
$response->assertStatus(302);
$contact->refresh();
$photo = $contact->photos->first();
$contact2 = factory(Contact::class)->create([
'account_id' => $user->account_id,
]);
$response = $this->post('/people/'.$contact2->hashID().'/makeProfilePicture/'.$photo->id);
// Assert the photo has been added for the correct user.
$this->assertDatabaseHas('photos', [
'id' => $photo->id,
'account_id' => $user->account_id,
'original_filename' => 'avatar.jpg',
'new_filename' => 'photos/'.$file->hashName(),
]);
$this->assertDatabaseHas('contacts', [
'id' => $contact->id,
'account_id' => $user->account_id,
'avatar_source' => 'photo',
]);
$this->assertDatabaseHas('contacts', [
'id' => $contact2->id,
'account_id' => $user->account_id,
'avatar_source' => 'photo',
]);
$this->assertDatabaseHas('contact_photo', [
'contact_id' => $contact->id,
'photo_id' => $photo->id,
]);
$this->assertDatabaseHas('contact_photo', [
'contact_id' => $contact2->id,
'photo_id' => $photo->id,
]);
}
}
+135
View File
@@ -0,0 +1,135 @@
<?php
namespace Tests\Feature;
use Tests\FeatureTestCase;
use App\Models\Account\Photo;
use App\Models\Contact\Contact;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class PhotosTest extends FeatureTestCase
{
use DatabaseTransactions;
protected $jsonStructure = [
'id',
'object',
'original_filename',
'new_filename',
'filesize',
'mime_type',
'link',
'contact',
'created_at',
'updated_at',
];
/**
* Returns an array containing a user object along with
* a contact for that user.
* @return array
*/
private function fetchUser()
{
$user = $this->signIn();
$contact = factory(Contact::class)->create([
'account_id' => $user->account_id,
]);
return [$user, $contact];
}
public function test_user_can_add_a_photo()
{
[$user, $contact] = $this->fetchUser();
Storage::fake('public');
$file = UploadedFile::fake()->image('avatar.jpg');
$params = [
'photo' => $file,
];
$response = $this->post('/people/'.$contact->hashID().'/photos', $params);
$response->assertStatus(201);
$response->assertJsonStructure([
'data' => $this->jsonStructure,
]);
// Assert the photo has been added for the correct user.
$this->assertDatabaseHas('photos', [
'account_id' => $user->account_id,
'original_filename' => 'avatar.jpg',
'new_filename' => 'photos/'.$file->hashName(),
]);
$this->assertDatabaseHas('contact_photo', [
'contact_id' => $contact->id,
'photo_id' => $response->json('data.id'),
]);
Storage::disk('public')->assertExists('photos/'.$file->hashName());
}
public function test_user_can_delete_a_photo()
{
[$user, $contact] = $this->fetchUser();
Storage::fake('public');
$file = UploadedFile::fake()->image('avatar.jpg');
$params = [
'photo' => $file,
];
$response1 = $this->post('/people/'.$contact->hashID().'/photos', $params);
$response2 = $this->delete('/people/'.$contact->hashID().'/photos/'.$response1->json('data.id'));
$response2->assertStatus(200);
$this->assertDatabaseMissing('photos', [
'account_id' => $user->account_id,
'original_filename' => 'avatar.jpg',
'new_filename' => 'photos/'.$file->hashName(),
]);
$this->assertDatabaseMissing('contact_photo', [
'contact_id' => $contact->id,
'photo_id' => $response1->json('data.id'),
]);
}
public function test_user_can_delete_a_photo_even_if_it_s_already_deleted()
{
[$user, $contact] = $this->fetchUser();
Storage::fake('public');
$file = UploadedFile::fake()->image('avatar.jpg');
$params = [
'photo' => $file,
];
$response1 = $this->post('/people/'.$contact->hashID().'/photos', $params);
Storage::delete($response1->json('data.new_filename'));
$response2 = $this->delete('/people/'.$contact->hashID().'/photos/'.$response1->json('data.id'));
$response2->assertStatus(200);
$this->assertDatabaseMissing('photos', [
'account_id' => $user->account_id,
'original_filename' => 'avatar.jpg',
'new_filename' => 'photos/'.$file->hashName(),
]);
$this->assertDatabaseMissing('contact_photo', [
'contact_id' => $contact->id,
'photo_id' => $response1->json('data.id'),
]);
}
}
-21
View File
@@ -1,21 +0,0 @@
<?php
namespace Tests\Unit\Helpers;
use Tests\TestCase;
use App\Helpers\RandomHelper;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class RandomHelperTest extends TestCase
{
use DatabaseTransactions;
public function test_it_returns_a_unique_uuid()
{
$this->assertEquals(
36,
strlen(RandomHelper::uuid())
);
$this->assertIsString(RandomHelper::uuid());
}
}
+1 -1
View File
@@ -49,7 +49,7 @@ class ScheduleStayInTouchTest extends TestCase
$notifications = NotificationFacade::sent($user, StayInTouchEmail::class);
$message = $notifications[0]->toMail($user);
$this->assertContains('You asked to be reminded to stay in touch with John Doe every 5 days.', $message->introLines);
$this->assertStringContainsString('You asked to be reminded to stay in touch with John Doe every 5 days.', implode('', $message->introLines));
$this->assertDatabaseHas('contacts', [
'stay_in_touch_trigger_date' => '2017-01-06 07:00:00',
+47 -224
View File
@@ -16,11 +16,9 @@ use App\Models\Contact\Document;
use App\Models\Contact\Reminder;
use App\Models\Contact\LifeEvent;
use App\Models\Contact\Occupation;
use App\Models\Contact\ContactField;
use App\Models\Contact\Conversation;
use App\Models\Instance\SpecialDate;
use App\Notifications\StayInTouchEmail;
use App\Models\Contact\ContactFieldType;
use App\Models\Relationship\Relationship;
use App\Jobs\StayInTouch\ScheduleStayInTouch;
use App\Models\Relationship\RelationshipType;
@@ -456,24 +454,14 @@ class ContactTest extends FeatureTestCase
);
}
public function testGetAvatarColor()
public function test_it_sets_a_default_avatar_color()
{
$contact = new Contact;
$contact->default_avatar_color = '#fffeee';
$contact = factory(Contact::class)->create([]);
$contact->setAvatarColor();
$this->assertEquals(
'#fffeee',
$contact->getAvatarColor()
);
}
public function testSetAvatarColor()
{
$contact = factory(Contact::class)->make();
$this->assertEquals(
strlen($contact->default_avatar_color) == 7,
$contact->setAvatarColor()
7,
strlen($contact->default_avatar_color)
);
}
@@ -536,194 +524,52 @@ class ContactTest extends FeatureTestCase
);
}
public function testGetAvatarReturnsPath()
public function test_it_returns_the_url_of_the_avatar()
{
config(['filesystems.default' => 'public']);
// default
$contact = factory(Contact::class)->create([
'avatar_default_url' => 'defaultURL',
'avatar_source' => 'default',
]);
$contact = new Contact;
$contact->has_avatar = true;
$contact->avatar_file_name = 'h0FMvD2cA3r2Q1EtGiv7aq9yl5BoXH2KIenDsoGX.jpg';
$this->assertEquals(
asset('/storage/avatars/h0FMvD2cA3r2Q1EtGiv7aq9yl5BoXH2KIenDsoGX_100.jpg'),
$contact->getAvatarURL(100)
);
}
public function test_get_avatar_returns_null_if_not_set()
{
$contact = new Contact;
$this->assertNull(
$this->assertStringContainsString(
'storage/defaultURL',
$contact->getAvatarURL()
);
}
public function test_get_avatar_returns_gravatar()
{
$contact = new Contact;
$contact->gravatar_url = 'https://gravatar.com/url';
// adorable
$contact = factory(Contact::class)->create([
'avatar_adorable_url' => 'adorableURL',
'avatar_source' => 'adorable',
]);
$this->assertEquals(
'https://gravatar.com/url',
'adorableURL',
$contact->getAvatarURL()
);
}
public function test_gravatar_set_noemail()
{
$account = factory(Account::class)->create();
$contact = factory(Contact::class)->create(['account_id' => $account->id]);
$contactFieldType = factory(ContactFieldType::class)->create(['account_id' => $account->id]);
$contactField = factory(ContactField::class)->create([
'account_id' => $account->id,
'contact_id' => $contact->id,
'contact_field_type_id' => $contactFieldType->id,
// gravatar
$contact = factory(Contact::class)->create([
'avatar_gravatar_url' => 'gravatarURL',
'avatar_source' => 'gravatar',
]);
$contact->updateGravatar();
$this->assertNull($contact->getAvatarURL());
}
public function test_gravatar_set_emailnotexists()
{
$account = factory(Account::class)->create();
$contact = factory(Contact::class)->create(['account_id' => $account->id]);
$contactFieldType = factory(ContactFieldType::class)->create(['account_id' => $account->id]);
$contactField = factory(ContactField::class)->create([
'account_id' => $account->id,
'contact_id' => $contact->id,
'contact_field_type_id' => $contactFieldType->id,
'data' => 'verybademailthatwillneverexistbecauseitstoolong204827494@x.com',
]);
$contact->updateGravatar();
$this->assertNull($contact->getAvatarURL());
}
public function test_gravatar_set_emailbadformat()
{
$account = factory(Account::class)->create();
$contact = factory(Contact::class)->create(['account_id' => $account->id]);
$contactFieldType = factory(ContactFieldType::class)->create(['account_id' => $account->id]);
$contactField = factory(ContactField::class)->create([
'account_id' => $account->id,
'contact_id' => $contact->id,
'contact_field_type_id' => $contactFieldType->id,
'data' => ' bad%20<email@bad.com',
]);
$contact->updateGravatar();
$this->assertNull($contact->getAvatarURL());
}
public function test_gravatar_set_emailreal()
{
$account = factory(Account::class)->create();
$contact = factory(Contact::class)->create(['account_id' => $account->id]);
$contactFieldType = factory(ContactFieldType::class)->create(['account_id' => $account->id]);
$contactField = factory(ContactField::class)->create([
'account_id' => $account->id,
'contact_id' => $contact->id,
'contact_field_type_id' => $contactFieldType->id,
'data' => 'alexis@saettler.org',
]);
$contact->updateGravatar();
$url = $contact->getAvatarURL();
$this->assertNotNull($url);
$this->assertStringContainsString('s=250&d=mm&r=g', $url);
$this->assertStringContainsString('https://www.gravatar.com', $url);
}
public function test_gravatar_set_emailreal_multiple()
{
$account = factory(Account::class)->create();
$contact = factory(Contact::class)->create(['account_id' => $account->id]);
$contactFieldType = factory(ContactFieldType::class)->create(['account_id' => $account->id]);
$contactField = factory(ContactField::class)->create([
'account_id' => $account->id,
'contact_id' => $contact->id,
'contact_field_type_id' => $contactFieldType->id,
'data' => 'test@test.com',
]);
$contactField = factory(ContactField::class)->create([
'account_id' => $account->id,
'contact_id' => $contact->id,
'contact_field_type_id' => $contactFieldType->id,
'data' => 'alexis@saettler.org',
]);
$contact->updateGravatar();
$url = $contact->getAvatarURL();
$this->assertNotNull($url);
$this->assertStringContainsString('s=250&d=mm&r=g', $url);
$this->assertStringContainsString('https://www.gravatar.com', $url);
}
public function test_gravatar_set_emailreal_secure()
{
config(['app.env' => 'production']);
$account = factory(Account::class)->create();
$contact = factory(Contact::class)->create(['account_id' => $account->id]);
$contactFieldType = factory(ContactFieldType::class)->create(['account_id' => $account->id]);
$contactField = factory(ContactField::class)->create([
'account_id' => $account->id,
'contact_id' => $contact->id,
'contact_field_type_id' => $contactFieldType->id,
'data' => 'alexis@saettler.org',
]);
$contact->updateGravatar();
$url = $contact->getAvatarURL();
$this->assertNotNull($url);
$this->assertStringContainsString('s=250&d=mm&r=g', $url);
$this->assertStringContainsString('https://secure.gravatar.com', $url);
}
public function test_get_avatar_returns_external_url()
{
$contact = new Contact();
$contact->has_avatar = true;
$contact->avatar_location = 'external';
$contact->avatar_external_url = 'https://facebook.com/johndoe.png';
$this->assertEquals(
'https://facebook.com/johndoe.png',
'gravatarURL',
$contact->getAvatarURL()
);
}
public function test_get_avatar_source_returns_external_or_internal()
{
$contact = new Contact();
$contact->has_avatar = false;
$this->assertNull(
$contact->getAvatarSource()
);
$contact->has_avatar = true;
$contact->avatar_location = 'external';
// photo
$photo = factory(Photo::class)->create([
'account_id' => $contact->account_id,
]);
$contact->avatar_photo_id = $photo->id;
$contact->avatar_source = 'photo';
$contact->save();
$this->assertEquals(
'external',
$contact->getAvatarSource()
);
$contact->has_avatar = true;
$contact->avatar_location = 'public';
$this->assertEquals(
'internal',
$contact->getAvatarSource()
config('app.url').'/storage/'.$photo->new_filename,
$contact->getAvatarURL()
);
}
@@ -794,43 +640,6 @@ class ContactTest extends FeatureTestCase
]));
$this->assertEquals(100, $contact->totalOutstandingDebtAmount());
$contact->debts()->save(new Debt([
'in_debt' => 'yes',
'amount' => 300,
'account_id' => $contact->account_id,
'contact_id' => $contact->id,
]));
$this->assertEquals(-200, $contact->totalOutstandingDebtAmount());
$contact->debts()->save(new Debt([
'in_debt' => 'yes',
'amount' => 300,
'status' => 'complete',
'account_id' => $contact->account_id,
'contact_id' => $contact->id,
]));
$this->assertEquals(-200, $contact->totalOutstandingDebtAmount());
}
public function test_set_special_date_creates_a_date_and_saves_the_id()
{
$contact = factory(Contact::class)->create();
$this->assertNull($contact->setSpecialDate(null, 2010, 10, 10));
$this->assertNull($contact->birthday_special_date_id);
$specialDate = $contact->setSpecialDate('birthdate', 2010, 10, 10);
$this->assertNotNull($contact->birthday_special_date_id);
$specialDate = $contact->setSpecialDate('deceased_date', 2010, 10, 10);
$this->assertNotNull($contact->deceased_special_date_id);
$specialDate = $contact->setSpecialDate('first_met', 2010, 10, 10);
$this->assertNotNull($contact->first_met_special_date_id);
}
public function test_set_special_date_with_age_creates_a_date_and_saves_the_id()
@@ -1229,4 +1038,18 @@ class ContactTest extends FeatureTestCase
$contact->getAgeAtDeath()
);
}
public function test_it_gets_the_default_avatar_url_attribute()
{
$contact = factory(Contact::class)->create([
'avatar_default_url' => 'avatars/image.jpg',
]);
config(['filesystems.default' => 'public']);
$this->assertStringContainsString(
'avatars/image.jpg',
$contact->avatar_default_url
);
}
}
+2
View File
@@ -26,8 +26,10 @@ class PhotoTest extends TestCase
$contact = factory(Contact::class)->create();
$photo = factory(Photo::class)->create();
$contact->photos()->sync([$photo->id]);
$photo = factory(Photo::class)->create();
$contact->photos()->sync([$photo->id]);
$this->assertTrue($photo->contacts()->exists());
}
@@ -4,6 +4,7 @@ namespace Tests\Unit\Services\Contact\Document;
use Tests\TestCase;
use App\Models\Account\Photo;
use App\Models\Account\Account;
use App\Models\Contact\Contact;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
@@ -53,11 +54,12 @@ class DestroyPhotoTest extends TestCase
public function test_it_throws_a_photo_doesnt_exist()
{
$account = factory(Account::class)->create([]);
$photo = factory(Photo::class)->create([]);
$request = [
'account_id' => $photo->account->id,
'photo_id' => 3,
'account_id' => $account->id,
'photo_id' => $photo->id,
];
$this->expectException(ModelNotFoundException::class);
@@ -0,0 +1,64 @@
<?php
namespace Tests\Unit\Services\Contact\Avatar;
use Tests\TestCase;
use App\Models\Contact\Contact;
use Illuminate\Http\UploadedFile;
use Illuminate\Validation\ValidationException;
use App\Services\Contact\Avatar\GenerateDefaultAvatar;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class GenerateDefaultAvatarTest extends TestCase
{
use DatabaseTransactions;
public function test_it_generates_a_default_avatar()
{
$contact = factory(Contact::class)->create([
'default_avatar_color' => '#000',
]);
$request = [
'contact_id' => $contact->id,
];
$contact = app(GenerateDefaultAvatar::class)->execute($request);
$this->assertStringContainsString(
'avatars/',
$contact->avatar_default_url
);
}
public function test_it_fails_if_wrong_parameters_are_given()
{
$request = [];
$this->expectException(ValidationException::class);
app(GenerateDefaultAvatar::class)->execute($request);
}
public function test_it_replaces_existing_default_avatar()
{
$file = UploadedFile::fake()->image('image.png');
$contact = factory(Contact::class)->create([
'default_avatar_color' => '#fff',
'avatar_default_url' => $file->getPathname(),
]);
$this->assertFileExists($file->getPathname());
$request = [
'contact_id' => $contact->id,
];
$contact = app(GenerateDefaultAvatar::class)->execute($request);
$this->assertStringContainsString(
'avatars/',
$contact->avatar_default_url
);
}
}
@@ -0,0 +1,53 @@
<?php
namespace Tests\Unit\Services\Contact\Avatar;
use Tests\TestCase;
use Illuminate\Validation\ValidationException;
use App\Services\Contact\Avatar\GetAdorableAvatarURL;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class GetAdorableAvatarTest extends TestCase
{
use DatabaseTransactions;
public function test_it_returns_an_url()
{
$request = [
'uuid' => 'matt@wordpress.com',
'size' => 400,
];
$url = app(GetAdorableAvatarURL::class)->execute($request);
$this->assertEquals(
'https://api.adorable.io/avatars/400/matt@wordpress.com.png',
$url
);
}
public function test_it_returns_an_url_with_a_default_avatar_size()
{
$request = [
'uuid' => 'matt@wordpress.com',
];
$url = app(GetAdorableAvatarURL::class)->execute($request);
// should return an avatar of 200 px wide
$this->assertEquals(
'https://api.adorable.io/avatars/200/matt@wordpress.com.png',
$url
);
}
public function test_it_fails_if_wrong_parameters_are_given()
{
$request = [
'size' => 200,
];
$this->expectException(ValidationException::class);
app(GetAdorableAvatarURL::class)->execute($request);
}
}
@@ -0,0 +1,109 @@
<?php
namespace Tests\Unit\Services\Contact\Avatar;
use Tests\TestCase;
use App\Models\Contact\Contact;
use App\Models\Contact\ContactField;
use App\Models\Contact\ContactFieldType;
use Illuminate\Validation\ValidationException;
use App\Services\Contact\Avatar\GetAvatarsFromInternet;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class GetAvatarsFromInternetTest extends TestCase
{
use DatabaseTransactions;
public function test_it_returns_a_contact_object_with_avatars()
{
$contact = factory(Contact::class)->create([]);
$contactFieldType = factory(ContactFieldType::class)->create([
'account_id' => $contact->account->id,
]);
$contactField = factory(ContactField::class)->create([
'contact_id' => $contact->id,
'account_id' => $contact->account->id,
'contact_field_type_id' => $contactFieldType->id,
'data' => 'matt@wordpress.com',
]);
$request = [
'contact_id' => $contact->id,
];
$contact = app(GetAvatarsFromInternet::class)->execute($request);
$this->assertInstanceOf(
Contact::class,
$contact
);
$this->assertNotNull(
$contact->avatar_adorable_url
);
$this->assertNotNull(
$contact->avatar_gravatar_url
);
}
public function test_gravatar_is_null_if_contact_doesnt_have_an_email()
{
$contact = factory(Contact::class)->create([]);
$request = [
'contact_id' => $contact->id,
];
$contact = app(GetAvatarsFromInternet::class)->execute($request);
$this->assertNull(
$contact->avatar_gravatar_url
);
}
public function test_avatar_source_is_reset_and_set_to_adorable_if_gravatar_doesnt_exist_anymore()
{
$contact = factory(Contact::class)->create([
'avatar_source' => 'gravatar',
]);
$contactFieldType = factory(ContactFieldType::class)->create([
'account_id' => $contact->account->id,
]);
$contactField = factory(ContactField::class)->create([
'contact_id' => $contact->id,
'account_id' => $contact->account->id,
'contact_field_type_id' => $contactFieldType->id,
'data' => 'matt@wordpress.com',
]);
$request = [
'contact_id' => $contact->id,
];
$contact = app(GetAvatarsFromInternet::class)->execute($request);
// now we call the service again to reset the gravatar url
$contactField->delete();
$contact = app(GetAvatarsFromInternet::class)->execute($request);
$this->assertNull(
$contact->avatar_gravatar_url
);
$this->assertEquals(
'adorable',
$contact->avatar_source
);
}
public function test_it_fails_if_wrong_parameters_are_given()
{
$request = [
'size' => 200,
];
$this->expectException(ValidationException::class);
app(GetAvatarsFromInternet::class)->execute($request);
}
}
@@ -0,0 +1,80 @@
<?php
namespace Tests\Unit\Services\Contact\Avatar;
use Tests\TestCase;
use Illuminate\Validation\ValidationException;
use App\Services\Contact\Avatar\GetGravatarURL;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class GetGravatarTest extends TestCase
{
use DatabaseTransactions;
public function test_it_returns_an_url()
{
$request = [
'email' => 'matt@wordpress.com',
'size' => 400,
];
$url = app(GetGravatarURL::class)->execute($request);
$this->assertEquals(
'https://www.gravatar.com/avatar/5bbc9048a99ec78cdbc227770e707efb.jpg?s=400&d=mm&r=g',
$url
);
}
public function test_it_returns_an_url_with_a_small_avatar_size()
{
$request = [
'email' => 'matt@wordpress.com',
'size' => 80,
];
$url = app(GetGravatarURL::class)->execute($request);
$this->assertEquals(
'https://www.gravatar.com/avatar/5bbc9048a99ec78cdbc227770e707efb.jpg?s=80&d=mm&r=g',
$url
);
}
public function test_it_returns_an_url_with_a_default_avatar_size()
{
$request = [
'email' => 'matt@wordpress.com',
];
$url = app(GetGravatarURL::class)->execute($request);
// should return an avatar of 200 px wide
$this->assertEquals(
'https://www.gravatar.com/avatar/5bbc9048a99ec78cdbc227770e707efb.jpg?s=200&d=mm&r=g',
$url
);
}
public function test_it_returns_null_if_no_avatar_is_found()
{
$request = [
'email' => 'jlskjdfl@dskfjlsd.com',
];
// should return an avatar of 200 px wide
$this->assertNull(
app(GetGravatarURL::class)->execute($request)
);
}
public function test_it_fails_if_wrong_parameters_are_given()
{
$request = [
'size' => 200,
];
$this->expectException(ValidationException::class);
$url = app(GetGravatarURL::class)->execute($request);
}
}
@@ -0,0 +1,172 @@
<?php
namespace Tests\Unit\Services\Contact\Avatar;
use Tests\TestCase;
use App\Models\Account\Photo;
use App\Models\Account\Account;
use App\Models\Contact\Contact;
use App\Services\Contact\Avatar\UpdateAvatar;
use Illuminate\Validation\ValidationException;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class UpdateAvatarTest extends TestCase
{
use DatabaseTransactions;
public function test_it_updates_the_avatar_with_gravatar()
{
$contact = factory(Contact::class)->create([]);
$request = [
'account_id' => $contact->account->id,
'contact_id' => $contact->id,
'source' => 'gravatar',
];
$contact = app(UpdateAvatar::class)->execute($request);
$this->assertDatabaseHas('contacts', [
'id' => $contact->id,
'avatar_source' => 'gravatar',
]);
$this->assertInstanceOf(
Contact::class,
$contact
);
}
public function test_it_updates_the_avatar_with_default_avatar()
{
$contact = factory(Contact::class)->create([]);
$request = [
'account_id' => $contact->account->id,
'contact_id' => $contact->id,
'source' => 'default',
];
$contact = app(UpdateAvatar::class)->execute($request);
$this->assertDatabaseHas('contacts', [
'id' => $contact->id,
'avatar_source' => 'default',
]);
$this->assertInstanceOf(
Contact::class,
$contact
);
}
public function test_it_updates_the_avatar_with_adorable()
{
$contact = factory(Contact::class)->create([]);
$request = [
'account_id' => $contact->account->id,
'contact_id' => $contact->id,
'source' => 'adorable',
];
$contact = app(UpdateAvatar::class)->execute($request);
$this->assertDatabaseHas('contacts', [
'id' => $contact->id,
'avatar_source' => 'adorable',
]);
$this->assertInstanceOf(
Contact::class,
$contact
);
}
public function test_it_updates_the_avatar_with_existing_photo()
{
$contact = factory(Contact::class)->create([]);
$photo = factory(Photo::class)->create([
'account_id' => $contact->account_id,
]);
$request = [
'account_id' => $contact->account->id,
'contact_id' => $contact->id,
'source' => 'photo',
'photo_id' => $photo->id,
];
$contact = app(UpdateAvatar::class)->execute($request);
$this->assertDatabaseHas('contacts', [
'id' => $contact->id,
'avatar_source' => 'photo',
'avatar_photo_id' => $photo->id,
]);
$this->assertInstanceOf(
Contact::class,
$contact
);
}
public function test_it_fails_if_wrong_parameters_are_given()
{
$contact = factory(Contact::class)->create([]);
$request = [
'account_id' => $contact->account->id,
'contact_id' => $contact->id,
];
$this->expectException(ValidationException::class);
app(UpdateAvatar::class)->execute($request);
}
public function test_it_throws_an_exception_if_contact_not_linked_to_account()
{
$account = factory(Account::class)->create([]);
$contact = factory(Contact::class)->create([]);
$request = [
'account_id' => $account->id,
'contact_id' => $contact->id,
'source' => 'adorable',
];
$this->expectException(ModelNotFoundException::class);
app(UpdateAvatar::class)->execute($request);
}
public function test_it_throws_an_exception_if_photo_not_linked_to_account()
{
// Case: photo doesn't exist
$contact = factory(Contact::class)->create([]);
$request = [
'account_id' => $contact->account->id,
'contact_id' => $contact->id,
'source' => 'photo',
'photo_id' => 1234,
];
$this->expectException(ModelNotFoundException::class);
$contact = app(UpdateAvatar::class)->execute($request);
// Case: photo exists but belongs to another account
$photo = factory(Photo::class)->create([
'account_id' => 1234,
]);
$request = [
'account_id' => $contact->account->id,
'contact_id' => $contact->id,
'source' => 'photo',
'photo_id' => $photo->id,
];
$this->expectException(ModelNotFoundException::class);
app(UpdateAvatar::class)->execute($request);
}
}
@@ -42,6 +42,13 @@ class CreateContactTest extends TestCase
'first_name' => 'john',
]);
// check that a default color has been set
$this->assertNotNull($contact->default_avatar_color);
// check that the default avatar has been generated
$this->assertNotNull($contact->avatar_adorable_uuid);
$this->assertNotNull($contact->avatar_adorable_url);
$this->assertNotNull($contact->avatar_default_url);
$this->assertInstanceOf(
Contact::class,
$contact
@@ -55,7 +55,7 @@ class UpdateBirthdayInformationTest extends TestCase
'is_date_known' => false,
];
app(UpdateBirthdayInformation::class)->execute($request);
$contact = app(UpdateBirthdayInformation::class)->execute($request);
$this->assertDatabaseHas('contacts', [
'id' => $contact->id,
@@ -143,6 +143,8 @@ class UpdateBirthdayInformationTest extends TestCase
$contact = app(UpdateBirthdayInformation::class)->execute($request);
$specialDate = SpecialDate::where('contact_id', $contact->id)->first();
$this->assertNotNull($contact->birthday_reminder_id);
}
@@ -137,7 +137,7 @@ class UpdateDeceasedInformationTest extends TestCase
'add_reminder' => true,
];
app(UpdateDeceasedInformation::class)->execute($request);
$contact = app(UpdateDeceasedInformation::class)->execute($request);
$specialDate = SpecialDate::where('contact_id', $contact->id)->first();
$reminder = Reminder::where('contact_id', $contact->id)->first();
+20 -19
View File
@@ -32,7 +32,7 @@ class ExportVCardTest extends TestCase
]);
$vCard = new VCard();
$exportVCard = new ExportVCard();
$exportVCard = app(ExportVCard::class);
$this->invokePrivateMethod($exportVCard, 'exportNames', [$contact, $vCard]);
$this->assertCount(
@@ -52,7 +52,7 @@ class ExportVCardTest extends TestCase
]);
$vCard = new VCard();
$exportVCard = new ExportVCard();
$exportVCard = app(ExportVCard::class);
$this->invokePrivateMethod($exportVCard, 'exportNames', [$contact, $vCard]);
$this->assertCount(
@@ -72,7 +72,7 @@ class ExportVCardTest extends TestCase
]);
$vCard = new VCard();
$exportVCard = new ExportVCard();
$exportVCard = app(ExportVCard::class);
$this->invokePrivateMethod($exportVCard, 'exportGender', [$contact, $vCard]);
$this->assertCount(
@@ -207,10 +207,10 @@ class ExportVCardTest extends TestCase
$contact = factory(Contact::class)->create(['account_id' => $account->id]);
$vCard = new VCard();
$contact->has_avatar = false;
$contact->gravatar_url = 'gravatar';
$contact->avatar_source = 'gravatar';
$contact->avatar_gravatar_url = 'gravatar';
$exportVCard = new ExportVCard();
$exportVCard = app(ExportVCard::class);
$this->invokePrivateMethod($exportVCard, 'exportPhoto', [$contact, $vCard]);
$this->assertCount(
@@ -229,7 +229,7 @@ class ExportVCardTest extends TestCase
]);
$vCard = new VCard();
$exportVCard = new ExportVCard();
$exportVCard = app(ExportVCard::class);
$this->invokePrivateMethod($exportVCard, 'exportWorkInformation', [$contact, $vCard]);
$this->assertCount(
@@ -248,7 +248,7 @@ class ExportVCardTest extends TestCase
]);
$vCard = new VCard();
$exportVCard = new ExportVCard();
$exportVCard = app(ExportVCard::class);
$this->invokePrivateMethod($exportVCard, 'exportWorkInformation', [$contact, $vCard]);
$this->assertCount(
@@ -268,7 +268,7 @@ class ExportVCardTest extends TestCase
]);
$vCard = new VCard();
$exportVCard = new ExportVCard();
$exportVCard = app(ExportVCard::class);
$this->invokePrivateMethod($exportVCard, 'exportWorkInformation', [$contact, $vCard]);
$this->assertCount(
@@ -286,7 +286,7 @@ class ExportVCardTest extends TestCase
$contact->setSpecialDate('birthdate', 2000, 10, 5);
$vCard = new VCard();
$exportVCard = new ExportVCard();
$exportVCard = app(ExportVCard::class);
$this->invokePrivateMethod($exportVCard, 'exportBirthday', [$contact, $vCard]);
$this->assertCount(
@@ -302,7 +302,7 @@ class ExportVCardTest extends TestCase
$contact = factory(Contact::class)->create(['account_id' => $account->id]);
$vCard = new VCard();
$exportVCard = new ExportVCard();
$exportVCard = app(ExportVCard::class);
$this->invokePrivateMethod($exportVCard, 'exportContactFields', [$contact, $vCard]);
$this->assertCount(
@@ -324,7 +324,7 @@ class ExportVCardTest extends TestCase
'contact_field_type_id' => $contactFieldType->id,
]);
$exportVCard = new ExportVCard();
$exportVCard = app(ExportVCard::class);
$this->invokePrivateMethod($exportVCard, 'exportContactFields', [$contact, $vCard]);
$this->assertCount(
@@ -340,7 +340,7 @@ class ExportVCardTest extends TestCase
$contact = factory(Contact::class)->create(['account_id' => $account->id]);
$vCard = new VCard();
$exportVCard = new ExportVCard();
$exportVCard = app(ExportVCard::class);
$this->invokePrivateMethod($exportVCard, 'exportAddress', [$contact, $vCard]);
$this->assertCount(
@@ -367,7 +367,7 @@ class ExportVCardTest extends TestCase
'account_id' => $account->id,
]);
$exportVCard = new ExportVCard();
$exportVCard = app(ExportVCard::class);
$this->invokePrivateMethod($exportVCard, 'exportAddress', [$contact, $vCard]);
$this->assertCount(
@@ -381,13 +381,13 @@ class ExportVCardTest extends TestCase
public function test_vcard_prepares_an_almost_empty_vcard()
{
$account = factory(Account::class)->create();
$contact = factory(Contact::class)->create(['account_id' => $account->id]);
$contact = factory(Contact::class)->create(['account_id' => $account->id])->refresh();
$exportVCard = new ExportVCard();
$exportVCard = app(ExportVCard::class);
$vCard = $this->invokePrivateMethod($exportVCard, 'export', [$contact]);
$this->assertCount(
self::defaultPropsCount + 5,
self::defaultPropsCount + 6,
$vCard->children()
);
@@ -418,11 +418,12 @@ class ExportVCardTest extends TestCase
'contact_field_type_id' => $contactFieldType->id,
]);
$exportVCard = new ExportVCard();
$exportVCard = app(ExportVCard::class);
$contact = $contact->refresh();
$vCard = $this->invokePrivateMethod($exportVCard, 'export', [$contact]);
$this->assertCount(
self::defaultPropsCount + 8,
self::defaultPropsCount + 9,
$vCard->children()
);