From 13bd66e502226d61348e4f8c6a8ad0591874afda Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Tue, 9 Jun 2020 22:48:26 +0300 Subject: [PATCH 1/3] Added initials route --- CHANGES.md | 1 + app/controllers/api/avatars.php | 74 ++++++++++++++++++++++++- docs/references/avatars/get-initials.md | 3 + 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 docs/references/avatars/get-initials.md diff --git a/CHANGES.md b/CHANGES.md index fc0ef3ae8..eacde2537 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ - New route in Locale API to fetch a list of languages - Added Google Fonts to Appwrite for offline availability +- Added a new route in the Avatars API to get user initials avatar ## Bug Fixes diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index bc493f8cc..0b465a8d8 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -16,6 +16,7 @@ use BaconQrCode\Renderer\Image\ImagickImageBackEnd; use BaconQrCode\Renderer\RendererStyle\RendererStyle; use BaconQrCode\Writer; use Utopia\Config\Config; +use Utopia\Validator\HexColor; include_once __DIR__ . '/../shared/api.php'; @@ -385,7 +386,78 @@ $utopia->get('/v1/avatars/qr') $response ->addHeader('Expires', date('D, d M Y H:i:s', time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache ->setContentType('image/png') - ->send('', $writer->writeString($text)) + ->send($writer->writeString($text)) + ; + } + ); + +$utopia->get('/v1/avatars/initials') + ->desc('Get User Initials') + ->param('name', '', function () { return new Text(512); }, 'Full Name. When empty, current user name or email will be used.', true) + ->param('width', 500, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('height', 500, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('color', '', function () { return new HexColor(); }, 'Changes text color. By default a random color will be picked and stay will persistent to the given name.', true) + ->param('background', '', function () { return new HexColor(); }, 'Changes background color. By default a random color will be picked and stay will persistent to the given name.', true) + ->label('scope', 'avatars.read') + ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'avatars') + ->label('sdk.method', 'getInitials') + ->label('sdk.methodType', 'location') + ->label('sdk.description', '/docs/references/avatars/get-initials.md') + ->action( + function ($name, $width, $height, $color, $background) use ($response, $user) { + $themes = [ + ['color' => '#27005e', 'background' => '#e1d2f6'], // VIOLET + ['color' => '#5e2700', 'background' => '#f3d9c6'], // ORANGE + ['color' => '#006128', 'background' => '#c9f3c6'], // GREEN + ['color' => '#580061', 'background' => '#f2d1f5'], // FUSCHIA + ['color' => '#00365d', 'background' => '#c6e1f3'], // BLUE + ['color' => '#00075c', 'background' => '#d2d5f6'], // INDIGO + ['color' => '#610038', 'background' => '#f5d1e6'], // PINK + ['color' => '#386100', 'background' => '#dcf1bd'], // LIME + ['color' => '#615800', 'background' => '#f1ecba'], // YELLOW + ['color' => '#610008', 'background' => '#f6d2d5'] // RED + ]; + + $rand = rand(0, count($themes)-1); + + $name = (!empty($name)) ? $name : $user->getAttribute('name', $user->getAttribute('email', '')); + $words = explode(' ', strtoupper($name)); + $initials = null; + $code = 0; + + foreach ($words as $w) { + $initials .= (isset($w[0])) ? $w[0] : ''; + $code += (isset($w[0])) ? ord($w[0]) : 0; + } + + $rand = substr($code,-1); + $background = (!empty($background)) ? '#'.$background : $themes[$rand]['background']; + $color = (!empty($color)) ? '#'.$color : $themes[$rand]['color']; + + $image = new \Imagick(); + $draw = new \ImagickDraw(); + $fontSize = min($width, $height) / 2; + + $draw->setFont(__DIR__."/../../../public/fonts/poppins-v9-latin-600.ttf"); + $image->setFont(__DIR__."/../../../public/fonts/poppins-v9-latin-600.ttf"); + + $draw->setFillColor(new \ImagickPixel($color)); + $draw->setFontSize($fontSize); + + $draw->setTextAlignment(\Imagick::ALIGN_CENTER); + $draw->annotation($width / 1.97, ($height / 2) + ($fontSize / 3), $initials); + + $image->newImage($width, $height, $background); + $image->setImageFormat("png"); + $image->drawImage($draw); + + //$image->setImageCompressionQuality(9 - round(($quality / 100) * 9)); + + $response + ->addHeader('Expires', date('D, d M Y H:i:s', time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache + ->setContentType('image/png') + ->send($image->getImageBlob()) ; } ); \ No newline at end of file diff --git a/docs/references/avatars/get-initials.md b/docs/references/avatars/get-initials.md new file mode 100644 index 000000000..87dafd3af --- /dev/null +++ b/docs/references/avatars/get-initials.md @@ -0,0 +1,3 @@ +Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned. + +You can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials. \ No newline at end of file From 11b935756bd639df0a779a65fc7d0d28afcadd6b Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Tue, 9 Jun 2020 22:54:15 +0300 Subject: [PATCH 2/3] Added tests --- tests/e2e/Services/Avatars/AvatarsBase.php | 91 ++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/tests/e2e/Services/Avatars/AvatarsBase.php b/tests/e2e/Services/Avatars/AvatarsBase.php index 45b252e68..206ea0c40 100644 --- a/tests/e2e/Services/Avatars/AvatarsBase.php +++ b/tests/e2e/Services/Avatars/AvatarsBase.php @@ -411,4 +411,95 @@ trait AvatarsBase return []; } + + + public function testGetInitials() + { + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_GET, '/avatars/initials', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + + $response = $this->client->call(Client::METHOD_GET, '/avatars/initials', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'width' => 200, + 'height' => 200, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + + $response = $this->client->call(Client::METHOD_GET, '/avatars/initials', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'name' => 'W W', + 'width' => 200, + 'height' => 200, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + + $response = $this->client->call(Client::METHOD_GET, '/avatars/initials', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'name' => 'W W', + 'width' => 200, + 'height' => 200, + 'color' => 'ffffff', + 'background' => '000000', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/avatars/initials', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'name' => 'W W', + 'width' => 200000, + 'height' => 200, + 'color' => 'ffffff', + 'background' => '000000', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, '/avatars/initials', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'name' => 'W W', + 'width' => 200, + 'height' => 200, + 'color' => 'white', + 'background' => '000000', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, '/avatars/initials', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'name' => 'W W', + 'width' => 200, + 'height' => 200, + 'color' => 'ffffff', + 'background' => 'black', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } } \ No newline at end of file From 8e47027c3409fb8d515485059bcf7c39518c08bc Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Wed, 10 Jun 2020 00:31:15 +0300 Subject: [PATCH 3/3] Removed 3rd party avatar services from the console --- app/controllers/api/avatars.php | 7 ++- public/dist/scripts/app-all.js | 13 ++--- public/dist/scripts/app.js | 13 ++--- public/scripts/filters.js | 99 ++++++++++++++++++--------------- 4 files changed, 68 insertions(+), 64 deletions(-) diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 0b465a8d8..f2a3c7bbf 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -426,11 +426,16 @@ $utopia->get('/v1/avatars/initials') $initials = null; $code = 0; - foreach ($words as $w) { + foreach ($words as $key => $w) { $initials .= (isset($w[0])) ? $w[0] : ''; $code += (isset($w[0])) ? ord($w[0]) : 0; + + if($key == 1) { + break; + } } + $length = count($words); $rand = substr($code,-1); $background = (!empty($background)) ? '#'.$background : $themes[$rand]['background']; $color = (!empty($color)) ? '#'.$color : $themes[$rand]['color']; diff --git a/public/dist/scripts/app-all.js b/public/dist/scripts/app-all.js index 8acc3d90d..3b11ddc72 100644 --- a/public/dist/scripts/app-all.js +++ b/public/dist/scripts/app-all.js @@ -2551,15 +2551,10 @@ return k;} function J(k){k=k.replace(/rn/g,"n");let d="";for(let F=0;F127&&x<2048){d+=String.fromCharCode((x>>6)|192);d+=String.fromCharCode((x&63)|128);}else{d+=String.fromCharCode((x>>12)|224);d+=String.fromCharCode(((x>>6)&63)|128);d+=String.fromCharCode((x&63)|128);}}} return d;} let C=Array();let P,h,E,v,g,Y,X,W,V;let S=7,Q=12,N=17,M=22;let A=5,z=9,y=14,w=20;let o=4,m=11,l=16,j=23;let U=6,T=10,R=15,O=21;s=J(s);C=e(s);Y=1732584193;X=4023233417;W=2562383102;V=271733878;for(P=0;Pchar.charCodeAt(0)).reduce((a,b)=>a+b,0).toString();let themes=[{color:"27005e",background:"e1d2f6"},{color:"5e2700",background:"f3d9c6"},{color:"006128",background:"c9f3c6"},{color:"580061",background:"f2d1f5"},{color:"00365d",background:"c6e1f3"},{color:"00075c",background:"d2d5f6"},{color:"610038",background:"f5d1e6"},{color:"386100",background:"dcf1bd"},{color:"615800",background:"f1ecba"},{color:"610008",background:"f6d2d5"}];name=name.split(" ").map(function(n){if(!isNaN(parseFloat(n))&&isFinite(n)){return"";} -return n[0];}).join("")||"--";let background=themes[theme[theme.length-1]]["background"];let color=themes[theme[theme.length-1]]["color"];let def="https://ui-avatars.com/api/"+ -encodeURIComponent(name)+"/"+ -size+"/"+ -encodeURIComponent(background)+"/"+ -encodeURIComponent(color);return("//www.gravatar.com/avatar/"+ -MD5(email)+".jpg?s="+ -size+"&d="+ -encodeURIComponent(def));}).add("selectedCollection",function($value,router){return $value===router.params.collectionId?"selected":"";}).add("selectedDocument",function($value,router){return $value===router.params.documentId?"selected":"";}).add("localeString",function($value){$value=parseInt($value);return!Number.isNaN($value)?$value.toLocaleString():"";}).add("date",function($value,date){return date.format("Y-m-d",$value);}).add("date-time",function($value,date){return date.format("Y-m-d H:i",$value);}).add("date-text",function($value,date){return date.format("d M Y",$value);}).add("ms2hum",function($value){let temp=$value;const years=Math.floor(temp/31536000),days=Math.floor((temp%=31536000)/86400),hours=Math.floor((temp%=86400)/3600),minutes=Math.floor((temp%=3600)/60),seconds=temp%60;if(days||hours||seconds||minutes){return((years?years+"y ":"")+ +let i=B(Y)+B(X)+B(W)+B(V);return i.toLowerCase();};let size=element.dataset["size"]||80;let email=$value.email||$value||"";let name=$value.name||$value||"";name=(typeof name!=='string')?'--':name;let def="/v1/avatars/initials?project=console"+"&name="+ +encodeURIComponent(name)+"&width="+ +size+"&height="+ +size;return def;}).add("selectedCollection",function($value,router){return $value===router.params.collectionId?"selected":"";}).add("selectedDocument",function($value,router){return $value===router.params.documentId?"selected":"";}).add("localeString",function($value){$value=parseInt($value);return!Number.isNaN($value)?$value.toLocaleString():"";}).add("date",function($value,date){return date.format("Y-m-d",$value);}).add("date-time",function($value,date){return date.format("Y-m-d H:i",$value);}).add("date-text",function($value,date){return date.format("d M Y",$value);}).add("ms2hum",function($value){let temp=$value;const years=Math.floor(temp/31536000),days=Math.floor((temp%=31536000)/86400),hours=Math.floor((temp%=86400)/3600),minutes=Math.floor((temp%=3600)/60),seconds=temp%60;if(days||hours||seconds||minutes){return((years?years+"y ":"")+ (days?days+"d ":"")+ (hours?hours+"h ":"")+ (minutes?minutes+"m ":"")+ diff --git a/public/dist/scripts/app.js b/public/dist/scripts/app.js index 593cb4039..1ad48def3 100644 --- a/public/dist/scripts/app.js +++ b/public/dist/scripts/app.js @@ -267,15 +267,10 @@ return k;} function J(k){k=k.replace(/rn/g,"n");let d="";for(let F=0;F127&&x<2048){d+=String.fromCharCode((x>>6)|192);d+=String.fromCharCode((x&63)|128);}else{d+=String.fromCharCode((x>>12)|224);d+=String.fromCharCode(((x>>6)&63)|128);d+=String.fromCharCode((x&63)|128);}}} return d;} let C=Array();let P,h,E,v,g,Y,X,W,V;let S=7,Q=12,N=17,M=22;let A=5,z=9,y=14,w=20;let o=4,m=11,l=16,j=23;let U=6,T=10,R=15,O=21;s=J(s);C=e(s);Y=1732584193;X=4023233417;W=2562383102;V=271733878;for(P=0;Pchar.charCodeAt(0)).reduce((a,b)=>a+b,0).toString();let themes=[{color:"27005e",background:"e1d2f6"},{color:"5e2700",background:"f3d9c6"},{color:"006128",background:"c9f3c6"},{color:"580061",background:"f2d1f5"},{color:"00365d",background:"c6e1f3"},{color:"00075c",background:"d2d5f6"},{color:"610038",background:"f5d1e6"},{color:"386100",background:"dcf1bd"},{color:"615800",background:"f1ecba"},{color:"610008",background:"f6d2d5"}];name=name.split(" ").map(function(n){if(!isNaN(parseFloat(n))&&isFinite(n)){return"";} -return n[0];}).join("")||"--";let background=themes[theme[theme.length-1]]["background"];let color=themes[theme[theme.length-1]]["color"];let def="https://ui-avatars.com/api/"+ -encodeURIComponent(name)+"/"+ -size+"/"+ -encodeURIComponent(background)+"/"+ -encodeURIComponent(color);return("//www.gravatar.com/avatar/"+ -MD5(email)+".jpg?s="+ -size+"&d="+ -encodeURIComponent(def));}).add("selectedCollection",function($value,router){return $value===router.params.collectionId?"selected":"";}).add("selectedDocument",function($value,router){return $value===router.params.documentId?"selected":"";}).add("localeString",function($value){$value=parseInt($value);return!Number.isNaN($value)?$value.toLocaleString():"";}).add("date",function($value,date){return date.format("Y-m-d",$value);}).add("date-time",function($value,date){return date.format("Y-m-d H:i",$value);}).add("date-text",function($value,date){return date.format("d M Y",$value);}).add("ms2hum",function($value){let temp=$value;const years=Math.floor(temp/31536000),days=Math.floor((temp%=31536000)/86400),hours=Math.floor((temp%=86400)/3600),minutes=Math.floor((temp%=3600)/60),seconds=temp%60;if(days||hours||seconds||minutes){return((years?years+"y ":"")+ +let i=B(Y)+B(X)+B(W)+B(V);return i.toLowerCase();};let size=element.dataset["size"]||80;let email=$value.email||$value||"";let name=$value.name||$value||"";name=(typeof name!=='string')?'--':name;let def="/v1/avatars/initials?project=console"+"&name="+ +encodeURIComponent(name)+"&width="+ +size+"&height="+ +size;return def;}).add("selectedCollection",function($value,router){return $value===router.params.collectionId?"selected":"";}).add("selectedDocument",function($value,router){return $value===router.params.documentId?"selected":"";}).add("localeString",function($value){$value=parseInt($value);return!Number.isNaN($value)?$value.toLocaleString():"";}).add("date",function($value,date){return date.format("Y-m-d",$value);}).add("date-time",function($value,date){return date.format("Y-m-d H:i",$value);}).add("date-text",function($value,date){return date.format("d M Y",$value);}).add("ms2hum",function($value){let temp=$value;const years=Math.floor(temp/31536000),days=Math.floor((temp%=31536000)/86400),hours=Math.floor((temp%=86400)/3600),minutes=Math.floor((temp%=3600)/60),seconds=temp%60;if(days||hours||seconds||minutes){return((years?years+"y ":"")+ (days?days+"d ":"")+ (hours?hours+"h ":"")+ (minutes?minutes+"m ":"")+ diff --git a/public/scripts/filters.js b/public/scripts/filters.js index 6f08193df..dacb9f053 100644 --- a/public/scripts/filters.js +++ b/public/scripts/filters.js @@ -3,7 +3,7 @@ window.ls.filter if (!$value) { return ""; } - + // MD5 (Message-Digest Algorithm) by WebToolkit let MD5 = function(s) { function L(k, d) { @@ -216,59 +216,68 @@ window.ls.filter let email = $value.email || $value || ""; let name = $value.name || $value || ""; - name = (typeof name !== 'string') ? '' : name; + name = (typeof name !== 'string') ? '--' : name; - let theme = name - .split("") - .map(char => char.charCodeAt(0)) - .reduce((a, b) => a + b, 0) - .toString(); - let themes = [ - { color: "27005e", background: "e1d2f6" }, // VIOLET - { color: "5e2700", background: "f3d9c6" }, // ORANGE - { color: "006128", background: "c9f3c6" }, // GREEN - { color: "580061", background: "f2d1f5" }, // FUSCHIA - { color: "00365d", background: "c6e1f3" }, // BLUE - { color: "00075c", background: "d2d5f6" }, // INDIGO - { color: "610038", background: "f5d1e6" }, // PINK - { color: "386100", background: "dcf1bd" }, // LIME - { color: "615800", background: "f1ecba" }, // YELLOW - { color: "610008", background: "f6d2d5" } // RED - ]; + // let theme = name + // .split("") + // .map(char => char.charCodeAt(0)) + // .reduce((a, b) => a + b, 0) + // .toString(); + // let themes = [ + // { color: "27005e", background: "e1d2f6" }, // VIOLET + // { color: "5e2700", background: "f3d9c6" }, // ORANGE + // { color: "006128", background: "c9f3c6" }, // GREEN + // { color: "580061", background: "f2d1f5" }, // FUSCHIA + // { color: "00365d", background: "c6e1f3" }, // BLUE + // { color: "00075c", background: "d2d5f6" }, // INDIGO + // { color: "610038", background: "f5d1e6" }, // PINK + // { color: "386100", background: "dcf1bd" }, // LIME + // { color: "615800", background: "f1ecba" }, // YELLOW + // { color: "610008", background: "f6d2d5" } // RED + // ]; - name = - name - .split(" ") - .map(function(n) { - if (!isNaN(parseFloat(n)) && isFinite(n)) { - return ""; - } + // name = + // name + // .split(" ") + // .map(function(n) { + // if (!isNaN(parseFloat(n)) && isFinite(n)) { + // return ""; + // } - return n[0]; - }) - .join("") || "--"; + // return n[0]; + // }) + // .join("") || "--"; - let background = themes[theme[theme.length - 1]]["background"]; - let color = themes[theme[theme.length - 1]]["color"]; + // let background = themes[theme[theme.length - 1]]["background"]; + // let color = themes[theme[theme.length - 1]]["color"]; let def = - "https://ui-avatars.com/api/" + + "/v1/avatars/initials?project=console"+ + "&name=" + encodeURIComponent(name) + - "/" + + "&width=" + size + - "/" + - encodeURIComponent(background) + - "/" + - encodeURIComponent(color); + "&height=" + + size; - return ( - "//www.gravatar.com/avatar/" + - MD5(email) + - ".jpg?s=" + - size + - "&d=" + - encodeURIComponent(def) - ); + return def; + // let def = + // "https://ui-avatars.com/api/" + + // encodeURIComponent(name) + + // "/" + + // size + + // "/" + + // encodeURIComponent(background) + + // "/" + + // encodeURIComponent(color); + // return ( + // "//www.gravatar.com/avatar/" + + // MD5(email) + + // ".jpg?s=" + + // size + + // "&d=" + + // encodeURIComponent(def) + // ); }) .add("selectedCollection", function($value, router) { return $value === router.params.collectionId ? "selected" : "";